feat(identity): enhance job display and narrative probes for identity roles
- Introduce IdentityJobDisplayLabel to normalize job labels - Update harness scenarios to reflect identity role changes - Implement probes for live lover recovery and episode continuity - Refactor narrative presentation to improve event relevance and tracking - Expand InterestVariety to manage routine replay cooldowns effectively
This commit is contained in:
parent
fcf2445cd0
commit
af17d9acd1
12 changed files with 354 additions and 19 deletions
|
|
@ -177,6 +177,32 @@ public static class ActivityAssetCatalog
|
|||
return FormatCitizenJobIdLabel(asset.id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stable identity role for the dossier nametag. Simulation variants such as
|
||||
/// <c>miner_deposit</c> describe the current assignment, not a distinct profession.
|
||||
/// Activity/task prose may still use the full job label.
|
||||
/// </summary>
|
||||
public static string IdentityJobDisplayLabel(string jobId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(jobId))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
string id = jobId.Trim();
|
||||
const string depositSuffix = "_deposit";
|
||||
if (id.EndsWith(depositSuffix, StringComparison.OrdinalIgnoreCase)
|
||||
&& id.Length > depositSuffix.Length)
|
||||
{
|
||||
id = id.Substring(0, id.Length - depositSuffix.Length);
|
||||
}
|
||||
|
||||
return JobDisplayLabel(id);
|
||||
}
|
||||
|
||||
public static string IdentityJobDisplayLabel(CitizenJobAsset asset) =>
|
||||
asset == null ? "" : IdentityJobDisplayLabel(asset.id);
|
||||
|
||||
/// <summary>
|
||||
/// Title Case + discovered resource-role reorder for a citizen job id.
|
||||
/// Public for harness audits over the full live library.
|
||||
|
|
|
|||
|
|
@ -2941,19 +2941,32 @@ public static class AgentHarness
|
|||
case "narrative_iteration_probe":
|
||||
{
|
||||
bool contextOk = CaptionComposer.HarnessProbeContextPolicy(out string context);
|
||||
bool spineOk = WatchCaption.HarnessProbeStorySpineRelevance(out string spine);
|
||||
bool combatOk = StickyScoreboard.HarnessProbeReasonStability(out string combat);
|
||||
bool noveltyOk = InterestVariety.HarnessProbeRoutineReplay(out string novelty);
|
||||
bool participantOk =
|
||||
NarrativePresentationFactory.HarnessProbeNamedParticipantFallback(
|
||||
out string participant);
|
||||
bool loverOk =
|
||||
HappinessEventRouter.HarnessProbeLiveLoverRecoveryPolicy(out string lover);
|
||||
bool identityOk =
|
||||
UnitDossier.HarnessProbeIdentityJobPresentation(out string identity);
|
||||
bool episodeOk =
|
||||
StoryScheduler.HarnessProbeEpisodeContinuity(out string episode);
|
||||
bool proseOk = EventReason.HarnessProbePresentationProse(out string prose);
|
||||
bool persistenceOk = NarrativePersistence.HarnessRoundTrip(out string persistence);
|
||||
bool ok = contextOk && combatOk && participantOk && proseOk && persistenceOk;
|
||||
bool ok = contextOk && spineOk && combatOk && noveltyOk
|
||||
&& participantOk && loverOk && identityOk && episodeOk
|
||||
&& proseOk && persistenceOk;
|
||||
if (ok) _cmdOk++; else _cmdFail++;
|
||||
Emit(
|
||||
cmd,
|
||||
ok,
|
||||
detail: "context={" + context + "} combat={" + combat
|
||||
+ "} participant={" + participant + "} prose={" + prose
|
||||
detail: "context={" + context + "} spine={" + spine
|
||||
+ "} combat={" + combat + "} novelty={" + novelty
|
||||
+ "} participant={" + participant + "} lover={" + lover
|
||||
+ "} identity={" + identity + "} episode={" + episode
|
||||
+ "} prose={" + prose
|
||||
+ "} persistence={" + persistence + "}");
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -296,6 +296,24 @@ namespace IdleSpectator;
|
|||
return proposed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When structural hysteresis keeps the previous named duel, its existing principals
|
||||
/// must remain camera principals too. Otherwise the identity changes a frame before
|
||||
/// the held "A vs B" reason catches up.
|
||||
/// </summary>
|
||||
public static bool HoldsPresentedDuelPrincipals(
|
||||
string previous,
|
||||
string proposed,
|
||||
string presented,
|
||||
bool allowImmediate)
|
||||
{
|
||||
return !allowImmediate
|
||||
&& !string.IsNullOrEmpty(previous)
|
||||
&& previous.StartsWith("Duel", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(previous, proposed ?? "", StringComparison.Ordinal)
|
||||
&& string.Equals(previous, presented ?? "", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static bool IsCombatReason(string label)
|
||||
{
|
||||
string value = (label ?? "").Trim();
|
||||
|
|
@ -319,14 +337,21 @@ namespace IdleSpectator;
|
|||
|
||||
sticky.Clear();
|
||||
string immediate = StabilizeCombatReason(sticky, duel, battle, 20f, true);
|
||||
bool keepsPrincipals = HoldsPresentedDuelPrincipals(
|
||||
duel, "Duel - Cato vs Dena", duel, allowImmediate: false);
|
||||
bool immediateMoves = !HoldsPresentedDuelPrincipals(
|
||||
duel, battle, battle, allowImmediate: true);
|
||||
bool ok = first == duel
|
||||
&& held == duel
|
||||
&& committed == battle
|
||||
&& headcount == battleTick
|
||||
&& immediate == battle;
|
||||
&& immediate == battle
|
||||
&& keepsPrincipals
|
||||
&& immediateMoves;
|
||||
detail = "first='" + first + "' held='" + held
|
||||
+ "' committed='" + committed + "' headcount='" + headcount
|
||||
+ "' immediate='" + immediate + "' pass=" + ok;
|
||||
+ "' immediate='" + immediate + "' principals="
|
||||
+ keepsPrincipals + "/" + immediateMoves + " pass=" + ok;
|
||||
return ok;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -230,6 +230,14 @@ public static class HappinessEventRouter
|
|||
}
|
||||
}
|
||||
|
||||
// Apply-site context can be absent when the status callback arrives outside the
|
||||
// relationship mutation stack. For explicitly lover-scoped effects, recover only the
|
||||
// subject's actual current lover; never guess from proximity or names.
|
||||
if (related == null && ShouldRecoverLiveLover(role, entry.RelationRole))
|
||||
{
|
||||
related = ActorRelation.GetLover(subject);
|
||||
}
|
||||
|
||||
if (role == HappinessRelationRole.None)
|
||||
{
|
||||
role = entry.RelationRole != HappinessRelationRole.None
|
||||
|
|
@ -688,6 +696,31 @@ public static class HappinessEventRouter
|
|||
return true;
|
||||
}
|
||||
|
||||
private static bool ShouldRecoverLiveLover(
|
||||
HappinessRelationRole supplied,
|
||||
HappinessRelationRole catalog)
|
||||
{
|
||||
return supplied == HappinessRelationRole.Lover
|
||||
|| catalog == HappinessRelationRole.Lover;
|
||||
}
|
||||
|
||||
public static bool HarnessProbeLiveLoverRecoveryPolicy(out string detail)
|
||||
{
|
||||
bool supplied = ShouldRecoverLiveLover(
|
||||
HappinessRelationRole.Lover, HappinessRelationRole.None);
|
||||
bool catalog = ShouldRecoverLiveLover(
|
||||
HappinessRelationRole.None, HappinessRelationRole.Lover);
|
||||
bool griefRejected = !ShouldRecoverLiveLover(
|
||||
HappinessRelationRole.DeceasedLover, HappinessRelationRole.DeceasedLover);
|
||||
bool unrelatedRejected = !ShouldRecoverLiveLover(
|
||||
HappinessRelationRole.None, HappinessRelationRole.ConversationPartner);
|
||||
bool ok = supplied && catalog && griefRejected && unrelatedRejected;
|
||||
detail = "supplied=" + supplied + " catalog=" + catalog
|
||||
+ " grief=" + griefRejected + " unrelated=" + unrelatedRejected
|
||||
+ " pass=" + ok;
|
||||
return ok;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A bridge callback can know the named partner after the first canonical signal was
|
||||
/// recorded. The drain stores occurrence references, so enriching the earlier survivor
|
||||
|
|
|
|||
|
|
@ -5646,7 +5646,8 @@ internal static class HarnessScenarios
|
|||
Step("act16k5", "dossier_force_job", value: "gatherer_bushes"),
|
||||
Step("act16k6", "assert", expect: "dossier_contains", value: "Bush Gatherer"),
|
||||
Step("act16k7", "dossier_force_job", value: "miner_deposit"),
|
||||
Step("act16k8", "assert", expect: "dossier_contains", value: "Miner Deposit"),
|
||||
Step("act16k8", "assert", expect: "dossier_contains", value: "Human/Miner"),
|
||||
Step("act16k9", "assert", expect: "caption_not_contains", value: "Deposit"),
|
||||
Step("act16l", "screenshot", value: "hud-dossier-job-only.png"),
|
||||
Step("act16m", "wait", wait: 0.45f),
|
||||
Step("act16n", "dossier_force_job", value: "farmer", label: "Story"),
|
||||
|
|
|
|||
|
|
@ -570,12 +570,27 @@ public static partial class InterestDirector
|
|||
"Battle", StringComparison.OrdinalIgnoreCase)
|
||||
|| (label ?? "").StartsWith(
|
||||
"Mass", StringComparison.OrdinalIgnoreCase));
|
||||
string proposedLabel = label ?? "";
|
||||
bool allowImmediateReason = staleNamedPair || promoteCollective;
|
||||
label = StickyScoreboard.StabilizeCombatReason(
|
||||
_current.Sticky,
|
||||
previousLabel,
|
||||
label,
|
||||
Time.unscaledTime,
|
||||
staleNamedPair || promoteCollective);
|
||||
allowImmediateReason);
|
||||
|
||||
// A held named-duel reason and a newly proposed focus are one editorial transition.
|
||||
// Keep both old principals until the reason commits, then move identity + reason together.
|
||||
if (StickyScoreboard.HoldsPresentedDuelPrincipals(
|
||||
previousLabel, proposedLabel, label, allowImmediateReason)
|
||||
&& curFollow != null
|
||||
&& curFollow.isAlive()
|
||||
&& curRelated != null
|
||||
&& curRelated.isAlive())
|
||||
{
|
||||
best = curFollow;
|
||||
foe = curRelated;
|
||||
}
|
||||
|
||||
bool followChanged = _current.FollowUnit != best;
|
||||
bool labelChanged = !string.Equals(previousLabel, label ?? "", StringComparison.Ordinal);
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ public static class InterestVariety
|
|||
public static float FixedDwellBeatCooldownSeconds =>
|
||||
Mathf.Max(HardCooldownSeconds, InterestScoringConfig.W.fixedDwellBeatCooldownSeconds);
|
||||
public static float EditorialReplayCooldownSeconds => Mathf.Max(HardCooldownSeconds, 24f);
|
||||
public static float RoutineReplayCooldownSeconds =>
|
||||
Mathf.Max(FixedDwellBeatCooldownSeconds, 120f);
|
||||
|
||||
private static readonly List<InterestLeadKind> RecentMix = new List<InterestLeadKind>(32);
|
||||
private static readonly List<string> RecentArcs = new List<string>(32);
|
||||
|
|
@ -176,6 +178,14 @@ public static class InterestVariety
|
|||
Time.unscaledTime + EditorialReplayCooldownSeconds);
|
||||
}
|
||||
|
||||
string routineKey = RoutineReplayKey(chosen);
|
||||
if (!string.IsNullOrEmpty(routineKey))
|
||||
{
|
||||
StampCooldown(
|
||||
routineKey,
|
||||
Time.unscaledTime + RoutineReplayCooldownSeconds);
|
||||
}
|
||||
|
||||
// Soft life chapters cool ledger heat so the same villagers do not monopolize.
|
||||
// Soft-life cool no longer demotes saga MCs (CharacterLedger removed).
|
||||
}
|
||||
|
|
@ -241,6 +251,74 @@ public static class InterestVariety
|
|||
return ok;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Routine texture may recur in the simulation, but the editor should not rediscover the
|
||||
/// same person's sleep/play/laugh/status beat every short cooldown cycle.
|
||||
/// </summary>
|
||||
public static bool IsRoutineReplayCooled(InterestCandidate c, float now = -1f)
|
||||
{
|
||||
string key = RoutineReplayKey(c);
|
||||
if (string.IsNullOrEmpty(key))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (now < 0f)
|
||||
{
|
||||
now = Time.unscaledTime;
|
||||
}
|
||||
|
||||
return CooldownUntil.TryGetValue(key, out float until) && now < until;
|
||||
}
|
||||
|
||||
public static bool HarnessProbeRoutineReplay(out string detail)
|
||||
{
|
||||
Clear();
|
||||
var sleeping = new InterestCandidate
|
||||
{
|
||||
SubjectId = 41,
|
||||
StatusId = "sleeping",
|
||||
AssetId = "sleeping",
|
||||
Completion = InterestCompletionKind.StatusPhase,
|
||||
HasNarrativeFunction = true,
|
||||
NarrativeFunction = NarrativeFunction.CharacterTexture
|
||||
};
|
||||
NoteSelection(sleeping, updateMix: false);
|
||||
var same = new InterestCandidate
|
||||
{
|
||||
SubjectId = 41,
|
||||
StatusId = "sleeping",
|
||||
AssetId = "sleeping",
|
||||
Completion = InterestCompletionKind.StatusPhase,
|
||||
HasNarrativeFunction = true,
|
||||
NarrativeFunction = NarrativeFunction.CharacterTexture
|
||||
};
|
||||
var other = new InterestCandidate
|
||||
{
|
||||
SubjectId = 42,
|
||||
StatusId = "sleeping",
|
||||
AssetId = "sleeping",
|
||||
Completion = InterestCompletionKind.StatusPhase,
|
||||
HasNarrativeFunction = true,
|
||||
NarrativeFunction = NarrativeFunction.CharacterTexture
|
||||
};
|
||||
var love = new InterestCandidate
|
||||
{
|
||||
SubjectId = 41,
|
||||
HappinessEffectId = "fallen_in_love",
|
||||
HasNarrativeFunction = true,
|
||||
NarrativeFunction = NarrativeFunction.ThreadOpener
|
||||
};
|
||||
bool sameCooled = IsRoutineReplayCooled(same);
|
||||
bool otherOpen = !IsRoutineReplayCooled(other);
|
||||
bool storyOpen = !IsRoutineReplayCooled(love);
|
||||
bool ok = sameCooled && otherOpen && storyOpen;
|
||||
detail = "same=" + sameCooled + " other=" + otherOpen
|
||||
+ " story=" + storyOpen + " pass=" + ok;
|
||||
Clear();
|
||||
return ok;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Score to subtract when this candidate matches the last shown variety arc.
|
||||
/// After one duel show, the next duel pays <c>repeatArcPenaltyPer</c>; a lover resets the streak.
|
||||
|
|
@ -717,6 +795,12 @@ public static class InterestVariety
|
|||
continue;
|
||||
}
|
||||
|
||||
if (IsRoutineReplayCooled(c, now))
|
||||
{
|
||||
InterestDropLog.Record("routine_replay", c.Label ?? c.Key);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c.LeadKind == InterestLeadKind.CharacterLed)
|
||||
{
|
||||
CharPool.Add(c);
|
||||
|
|
@ -947,6 +1031,32 @@ public static class InterestVariety
|
|||
return string.IsNullOrEmpty(key) ? "" : "replay:key:" + key;
|
||||
}
|
||||
|
||||
private static string RoutineReplayKey(InterestCandidate c)
|
||||
{
|
||||
if (c == null || c.SubjectId == 0)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
NarrativeFunction function = NarrativeFunctionPolicy.Classify(c);
|
||||
if (function != NarrativeFunction.CharacterTexture
|
||||
&& !IsRestLifeChapter(c)
|
||||
&& !IsSoftStatusFxChapter(c))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
string family = c.StatusId;
|
||||
if (string.IsNullOrEmpty(family)) family = c.HappinessEffectId;
|
||||
if (string.IsNullOrEmpty(family)) family = c.NarrativePresentation?.ActionId;
|
||||
if (string.IsNullOrEmpty(family)) family = c.AssetId;
|
||||
if (string.IsNullOrEmpty(family)) family = NormalizeBeatLabel(c.Label);
|
||||
family = (family ?? "").Trim().ToLowerInvariant();
|
||||
return string.IsNullOrEmpty(family)
|
||||
? ""
|
||||
: "routine:" + c.SubjectId + ":" + family;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parenthood / pregnancy / newborn family chapter - variety + ledger cool as a class.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ public static class EpisodeShotSelector
|
|||
bool harness = c != null && string.Equals(c.Source, "harness", System.StringComparison.OrdinalIgnoreCase);
|
||||
if (c == null || c.Selected
|
||||
|| InterestVariety.IsEditorialReplayCooled(c)
|
||||
|| InterestVariety.IsRoutineReplayCooled(c)
|
||||
|| !StoryScheduler.MayUseCombatShot(episode, c)
|
||||
|| (requirePresentable && !harness && !EventPresentability.WouldShow(c))) continue;
|
||||
NarrativeFunction function = NarrativeFunctionPolicy.Classify(c);
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ public static class StoryScheduler
|
|||
private const float EpisodeNoProgressSeconds = 14f;
|
||||
private const float PayoffSettleSeconds = 8f;
|
||||
private const float EpisodeMaxSeconds = 75f;
|
||||
private const float EarlyEpisodeSwitchMargin = 30f;
|
||||
private const float EstablishedEpisodeSwitchMargin = 20f;
|
||||
public const int EpisodeCombatShotBudget = 3;
|
||||
private static readonly List<NarrativeThread> OpenThreads = new List<NarrativeThread>(32);
|
||||
private static readonly List<InterestCandidate> Pending = new List<InterestCandidate>(96);
|
||||
|
|
@ -377,6 +379,27 @@ public static class StoryScheduler
|
|||
return ok;
|
||||
}
|
||||
|
||||
public static bool HarnessProbeEpisodeContinuity(out string detail)
|
||||
{
|
||||
var early = new EpisodePlan { GoalMet = false };
|
||||
early.CoveredRoles.Add(EpisodeShotRole.Establish);
|
||||
var established = new EpisodePlan { GoalMet = false };
|
||||
established.CoveredRoles.Add(EpisodeShotRole.Establish);
|
||||
established.CoveredRoles.Add(EpisodeShotRole.Development);
|
||||
var complete = new EpisodePlan { GoalMet = true };
|
||||
float earlyMargin = ThreadSwitchMargin(early);
|
||||
float establishedMargin = ThreadSwitchMargin(established);
|
||||
float completeMargin = ThreadSwitchMargin(complete);
|
||||
bool ok = earlyMargin > establishedMargin
|
||||
&& establishedMargin == EstablishedEpisodeSwitchMargin
|
||||
&& completeMargin == EstablishedEpisodeSwitchMargin;
|
||||
detail = "margin=" + earlyMargin.ToString("0")
|
||||
+ "/" + establishedMargin.ToString("0")
|
||||
+ "/" + completeMargin.ToString("0")
|
||||
+ " pass=" + ok;
|
||||
return ok;
|
||||
}
|
||||
|
||||
public static float StoryPotential(long characterId)
|
||||
{
|
||||
CharacterStoryState state = NarrativeStoryStore.Character(characterId);
|
||||
|
|
@ -419,7 +442,20 @@ public static class StoryScheduler
|
|||
&& now - ActiveEpisode.LastProgressAt >= PayoffSettleSeconds;
|
||||
bool stale = ActiveEpisode != null
|
||||
&& now - ActiveEpisode.LastProgressAt >= EpisodeNoProgressSeconds;
|
||||
return settled || stale || bestScore >= activeScore + 20f ? best : active;
|
||||
float switchMargin = ThreadSwitchMargin(ActiveEpisode);
|
||||
return settled || stale || bestScore >= activeScore + switchMargin ? best : active;
|
||||
}
|
||||
|
||||
private static float ThreadSwitchMargin(EpisodePlan episode)
|
||||
{
|
||||
if (episode != null
|
||||
&& !episode.GoalMet
|
||||
&& episode.CoveredRoles.Count < 2)
|
||||
{
|
||||
return EarlyEpisodeSwitchMargin;
|
||||
}
|
||||
|
||||
return EstablishedEpisodeSwitchMargin;
|
||||
}
|
||||
|
||||
private static bool EpisodeStillValid(EpisodePlan episode, NarrativeThread thread, float now)
|
||||
|
|
|
|||
|
|
@ -327,6 +327,9 @@ public static class CaptionComposer
|
|||
}
|
||||
|
||||
string tipMood = TipMood(beatLine, tip);
|
||||
// A return to an MC may show a biography reminder only when there is no authored
|
||||
// orange beat. Reentry must never let an unrelated legacy fact hitch onto a live event.
|
||||
bool biographyReentry = isReentry && string.IsNullOrEmpty(beatLine);
|
||||
// Walk strongest → weaker so a soft tip can skip a kill/role hitch and still
|
||||
// show an ongoing war/plot/rival stake when one exists.
|
||||
LifeSagaFact stake = PickHitchStake(
|
||||
|
|
@ -335,7 +338,7 @@ public static class CaptionComposer
|
|||
beatLine,
|
||||
tipMood,
|
||||
identityLine ?? "",
|
||||
isReentry);
|
||||
biographyReentry);
|
||||
if (stake == null)
|
||||
{
|
||||
return "";
|
||||
|
|
@ -351,13 +354,13 @@ public static class CaptionComposer
|
|||
string beatLine,
|
||||
string tipMood,
|
||||
string identityLine,
|
||||
bool isReentry)
|
||||
bool biographyReentry)
|
||||
{
|
||||
LifeSagaFact best = LifeSagaMemory.StrongestUnresolved(focusId);
|
||||
if (best != null
|
||||
&& !BeatCoversStake(beatLine, best)
|
||||
&& !ShouldOmitStakeHitch(best, tipMood, identityLine)
|
||||
&& StakeBelongsToBeat(best, tip, tipMood, isReentry))
|
||||
&& StakeBelongsToBeat(best, tip, tipMood, biographyReentry))
|
||||
{
|
||||
return best;
|
||||
}
|
||||
|
|
@ -374,7 +377,7 @@ public static class CaptionComposer
|
|||
|
||||
if (BeatCoversStake(beatLine, f)
|
||||
|| ShouldOmitStakeHitch(f, tipMood, identityLine)
|
||||
|| !StakeBelongsToBeat(f, tip, tipMood, isReentry))
|
||||
|| !StakeBelongsToBeat(f, tip, tipMood, biographyReentry))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
|
@ -401,14 +404,14 @@ public static class CaptionComposer
|
|||
LifeSagaFact stake,
|
||||
InterestCandidate tip,
|
||||
string tipMood,
|
||||
bool isReentry)
|
||||
bool biographyReentry)
|
||||
{
|
||||
if (stake == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isReentry)
|
||||
if (biographyReentry)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
|
@ -629,6 +632,11 @@ public static class CaptionComposer
|
|||
Kind = LifeSagaFactKind.RivalEarned,
|
||||
OtherId = 44
|
||||
};
|
||||
var kill = new LifeSagaFact
|
||||
{
|
||||
Kind = LifeSagaFactKind.Kill,
|
||||
OtherId = 55
|
||||
};
|
||||
var texture = new InterestCandidate
|
||||
{
|
||||
HasNarrativeFunction = true,
|
||||
|
|
@ -651,23 +659,32 @@ public static class CaptionComposer
|
|||
NarrativeFunction = NarrativeFunction.Escalation,
|
||||
Completion = InterestCompletionKind.CombatActive
|
||||
};
|
||||
var outbreak = new InterestCandidate
|
||||
{
|
||||
HasNarrativeFunction = true,
|
||||
NarrativeFunction = NarrativeFunction.WorldContext,
|
||||
Completion = InterestCompletionKind.StatusOutbreak
|
||||
};
|
||||
|
||||
bool textureRejectsResume = !StakeBelongsToBeat(founding, texture, "", false);
|
||||
bool childMatchesChild = StakeBelongsToBeat(child, childBeat, "love", false);
|
||||
bool childRejectsBond = !StakeBelongsToBeat(bond, childBeat, "love", false);
|
||||
bool combatMatchesRival = StakeBelongsToBeat(rival, combat, "combat", false);
|
||||
bool reentryRecalls = StakeBelongsToBeat(founding, texture, "", true);
|
||||
bool outbreakRejectsKill = !StakeBelongsToBeat(kill, outbreak, "", false);
|
||||
bool restRejectsResume = !StakeBelongsToBeat(founding, texture, "rest", false);
|
||||
bool ok = textureRejectsResume
|
||||
&& childMatchesChild
|
||||
&& childRejectsBond
|
||||
&& combatMatchesRival
|
||||
&& reentryRecalls
|
||||
&& outbreakRejectsKill
|
||||
&& restRejectsResume;
|
||||
detail = "texture=" + textureRejectsResume
|
||||
+ " child=" + childMatchesChild + "/" + childRejectsBond
|
||||
+ " combat=" + combatMatchesRival
|
||||
+ " reentry=" + reentryRecalls
|
||||
+ " outbreak=" + outbreakRejectsKill
|
||||
+ " rest=" + restRejectsResume
|
||||
+ " pass=" + ok;
|
||||
return ok;
|
||||
|
|
|
|||
|
|
@ -150,15 +150,15 @@ public sealed class UnitDossier
|
|||
{
|
||||
if (!string.IsNullOrEmpty(HarnessJobLabelOverride))
|
||||
{
|
||||
// Same discovery path as live jobs (resource-role families, Title Case id).
|
||||
return ActivityAssetCatalog.JobDisplayLabel(HarnessJobLabelOverride.Trim());
|
||||
// Same discovery path as live jobs, including identity-only assignment collapse.
|
||||
return ActivityAssetCatalog.IdentityJobDisplayLabel(HarnessJobLabelOverride.Trim());
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (actor?.citizen_job != null)
|
||||
{
|
||||
string fromAsset = ActivityAssetCatalog.JobDisplayLabel(actor.citizen_job);
|
||||
string fromAsset = ActivityAssetCatalog.IdentityJobDisplayLabel(actor.citizen_job);
|
||||
if (!string.IsNullOrEmpty(fromAsset))
|
||||
{
|
||||
return fromAsset;
|
||||
|
|
@ -171,7 +171,18 @@ public sealed class UnitDossier
|
|||
}
|
||||
|
||||
string jobId = ActivityInterestTable.SafeJobId(actor);
|
||||
return ActivityAssetCatalog.JobDisplayLabel(jobId);
|
||||
return ActivityAssetCatalog.IdentityJobDisplayLabel(jobId);
|
||||
}
|
||||
|
||||
public static bool HarnessProbeIdentityJobPresentation(out string detail)
|
||||
{
|
||||
string job = ActivityAssetCatalog.IdentityJobDisplayLabel("miner_deposit");
|
||||
string tag = BuildIdentityTag("human", job);
|
||||
bool ok = job == "Miner"
|
||||
&& tag == "Human/Miner"
|
||||
&& tag.IndexOf("Deposit", StringComparison.OrdinalIgnoreCase) < 0;
|
||||
detail = "job='" + job + "' tag='" + tag + "' pass=" + ok;
|
||||
return ok;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1262,7 +1262,8 @@ public static class WatchCaption
|
|||
return spine;
|
||||
}
|
||||
|
||||
if (ReasonLeadsWithToken(reason, kind))
|
||||
if (ReasonLeadsWithToken(reason, kind)
|
||||
|| ReasonSemanticallyMatchesStoryKind(reason, kind))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
|
@ -1276,6 +1277,52 @@ public static class WatchCaption
|
|||
return spine;
|
||||
}
|
||||
|
||||
private static bool ReasonSemanticallyMatchesStoryKind(string reason, string kind)
|
||||
{
|
||||
string text = (reason ?? "").Trim().ToLowerInvariant();
|
||||
string story = (kind ?? "").Trim().ToLowerInvariant();
|
||||
if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(story))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (story)
|
||||
{
|
||||
case "love":
|
||||
return text.IndexOf("love", StringComparison.Ordinal) >= 0
|
||||
|| text.IndexOf("smitten", StringComparison.Ordinal) >= 0
|
||||
|| text.IndexOf("mates with", StringComparison.Ordinal) >= 0
|
||||
|| text.IndexOf("intimacy", StringComparison.Ordinal) >= 0;
|
||||
case "grief":
|
||||
return text.IndexOf("mourn", StringComparison.Ordinal) >= 0
|
||||
|| text.IndexOf("despair", StringComparison.Ordinal) >= 0
|
||||
|| text.IndexOf("grief", StringComparison.Ordinal) >= 0;
|
||||
case "duel":
|
||||
case "skirmish":
|
||||
case "battle":
|
||||
case "mass":
|
||||
return ReasonLeadsWithCombatScale(reason)
|
||||
|| text.IndexOf(" is fighting", StringComparison.Ordinal) >= 0;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool HarnessProbeStorySpineRelevance(out string detail)
|
||||
{
|
||||
string love = FilterRedundantStorySpine("Love · Climax", "Falls in love with Omyahel");
|
||||
string duel = FilterRedundantStorySpine("Duel · Climax", "Joon is fighting");
|
||||
string pregnancy = FilterRedundantStorySpine("Love · Climax", "Becomes pregnant");
|
||||
string aftermath = FilterRedundantStorySpine("Love · Aftermath", "Finds love with Omyahel");
|
||||
bool ok = string.IsNullOrEmpty(love)
|
||||
&& string.IsNullOrEmpty(duel)
|
||||
&& pregnancy == "Love · Climax"
|
||||
&& aftermath == "Love · Aftermath";
|
||||
detail = "love='" + love + "' duel='" + duel + "' pregnancy='" + pregnancy
|
||||
+ "' aftermath='" + aftermath + "' pass=" + ok;
|
||||
return ok;
|
||||
}
|
||||
|
||||
private static bool IsCombatSpineKind(string kind)
|
||||
{
|
||||
return string.Equals(kind, "Duel", StringComparison.OrdinalIgnoreCase)
|
||||
|
|
|
|||
Loading…
Reference in a new issue