reworking scoring
This commit is contained in:
parent
d260472cc4
commit
77cb688925
11 changed files with 982 additions and 165 deletions
|
|
@ -2436,7 +2436,9 @@ public static class AgentHarness
|
||||||
out float charSig,
|
out float charSig,
|
||||||
out float maxWatch,
|
out float maxWatch,
|
||||||
out float ttl,
|
out float ttl,
|
||||||
out bool resumable);
|
out bool resumable,
|
||||||
|
out int participants,
|
||||||
|
out int notables);
|
||||||
|
|
||||||
InterestLeadKind lead = leadOpt
|
InterestLeadKind lead = leadOpt
|
||||||
?? (tier >= InterestTier.Curiosity && tier < InterestTier.Action
|
?? (tier >= InterestTier.Curiosity && tier < InterestTier.Action
|
||||||
|
|
@ -2483,11 +2485,29 @@ public static class AgentHarness
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (participants > 0 || notables > 0)
|
||||||
|
{
|
||||||
|
c.ParticipantCount = participants;
|
||||||
|
c.NotableParticipantCount = notables;
|
||||||
|
if (participants >= 2 || c.AssetId == "live_battle")
|
||||||
|
{
|
||||||
|
c.Category = "Combat";
|
||||||
|
c.Verb = "fighting";
|
||||||
|
// Keep FixedDwell/Manual for harness injects - units are not actually fighting.
|
||||||
|
if (!force)
|
||||||
|
{
|
||||||
|
c.Completion = InterestCompletionKind.FixedDwell;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InterestScoring.RecalcTotal(c);
|
||||||
|
}
|
||||||
|
|
||||||
_cmdOk++;
|
_cmdOk++;
|
||||||
Emit(
|
Emit(
|
||||||
cmd,
|
cmd,
|
||||||
ok: true,
|
ok: true,
|
||||||
detail: $"injected key={c.Key} tier={c.Urgency} lead={c.LeadKind} evt={c.EventStrength:0.#} char={c.CharacterSignificance:0.#} pending={InterestRegistry.PendingCount}");
|
detail: $"injected key={c.Key} tier={c.Urgency} lead={c.LeadKind} evt={c.EventStrength:0.#} char={c.CharacterSignificance:0.#} n={c.ParticipantCount}/{c.NotableParticipantCount} score={c.TotalScore:0.#} pending={InterestRegistry.PendingCount}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ParseInterestInjectOptions(
|
private static void ParseInterestInjectOptions(
|
||||||
|
|
@ -2498,7 +2518,9 @@ public static class AgentHarness
|
||||||
out float charSig,
|
out float charSig,
|
||||||
out float maxWatch,
|
out float maxWatch,
|
||||||
out float ttl,
|
out float ttl,
|
||||||
out bool resumable)
|
out bool resumable,
|
||||||
|
out int participants,
|
||||||
|
out int notables)
|
||||||
{
|
{
|
||||||
force = false;
|
force = false;
|
||||||
lead = null;
|
lead = null;
|
||||||
|
|
@ -2507,6 +2529,8 @@ public static class AgentHarness
|
||||||
maxWatch = -1f;
|
maxWatch = -1f;
|
||||||
ttl = -1f;
|
ttl = -1f;
|
||||||
resumable = true;
|
resumable = true;
|
||||||
|
participants = 0;
|
||||||
|
notables = 0;
|
||||||
if (string.IsNullOrEmpty(raw))
|
if (string.IsNullOrEmpty(raw))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
|
@ -2584,6 +2608,15 @@ public static class AgentHarness
|
||||||
case "resumable":
|
case "resumable":
|
||||||
resumable = ParseBool(val, true);
|
resumable = ParseBool(val, true);
|
||||||
break;
|
break;
|
||||||
|
case "n":
|
||||||
|
case "participants":
|
||||||
|
case "fighters":
|
||||||
|
int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out participants);
|
||||||
|
break;
|
||||||
|
case "notables":
|
||||||
|
case "notable":
|
||||||
|
int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out notables);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3105,7 +3138,23 @@ public static class AgentHarness
|
||||||
bool captionHit = !string.IsNullOrEmpty(needle)
|
bool captionHit = !string.IsNullOrEmpty(needle)
|
||||||
&& caption.IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0;
|
&& caption.IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0;
|
||||||
pass = dossierHit || captionHit;
|
pass = dossierHit || captionHit;
|
||||||
detail = $"needle='{needle}' caption='{caption.Replace("\n", " | ")}' dossier={(dossier != null)}";
|
detail = $"needle='{needle}' caption='{caption.Replace("\n", " | ")}' dossier={(dossier != null)} reason='{dossier?.ReasonLine}'";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "dossier_not_contains":
|
||||||
|
{
|
||||||
|
string needle = string.IsNullOrEmpty(cmd.value) ? cmd.label : cmd.value;
|
||||||
|
UnitDossier dossier = WatchCaption.Current;
|
||||||
|
string reason = dossier?.ReasonLine ?? "";
|
||||||
|
string caption = WatchCaption.LastCaptionText ?? "";
|
||||||
|
bool hit = (!string.IsNullOrEmpty(needle) && reason.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0)
|
||||||
|
|| (!string.IsNullOrEmpty(needle) && caption.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0)
|
||||||
|
|| (dossier != null && dossier.ContainsIgnoreCase(needle));
|
||||||
|
// Prefer reason-row check: fail only if the reason line (or full dossier) still shows the bad phrase.
|
||||||
|
bool reasonHit = !string.IsNullOrEmpty(needle)
|
||||||
|
&& reason.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0;
|
||||||
|
pass = !reasonHit;
|
||||||
|
detail = $"needle_absent_in_reason='{needle}' reason='{reason}' hit={reasonHit}";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "dossier_task_refresh":
|
case "dossier_task_refresh":
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,12 @@ internal static class HarnessScenarios
|
||||||
case "director_gaps":
|
case "director_gaps":
|
||||||
case "director_coverage":
|
case "director_coverage":
|
||||||
return DirectorGaps();
|
return DirectorGaps();
|
||||||
|
case "watch_reason":
|
||||||
|
case "watch_reason_clarity":
|
||||||
|
return WatchReasonClarity();
|
||||||
|
case "director_action_priority":
|
||||||
|
case "action_priority":
|
||||||
|
return DirectorActionPriority();
|
||||||
case "discovery":
|
case "discovery":
|
||||||
return Discovery();
|
return Discovery();
|
||||||
case "world_log":
|
case "world_log":
|
||||||
|
|
@ -134,6 +140,7 @@ internal static class HarnessScenarios
|
||||||
Nested("reg_director_life", "director_lifecycle"),
|
Nested("reg_director_life", "director_lifecycle"),
|
||||||
Nested("reg_interest_happiness", "interest_happiness"),
|
Nested("reg_interest_happiness", "interest_happiness"),
|
||||||
Nested("reg_director_gaps", "director_gaps"),
|
Nested("reg_director_gaps", "director_gaps"),
|
||||||
|
Nested("reg_action_priority", "director_action_priority"),
|
||||||
Nested("reg_discovery", "discovery"),
|
Nested("reg_discovery", "discovery"),
|
||||||
Nested("reg_worldlog", "world_log"),
|
Nested("reg_worldlog", "world_log"),
|
||||||
Nested("reg_chronicle", "chronicle_smoke"),
|
Nested("reg_chronicle", "chronicle_smoke"),
|
||||||
|
|
@ -809,6 +816,110 @@ internal static class HarnessScenarios
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Watch-reason row must explain why the camera is here (event), not who (title/name).
|
||||||
|
/// </summary>
|
||||||
|
private static List<HarnessCommand> WatchReasonClarity()
|
||||||
|
{
|
||||||
|
return new List<HarnessCommand>
|
||||||
|
{
|
||||||
|
Step("wr0", "dismiss_windows"),
|
||||||
|
Step("wr1", "wait_world"),
|
||||||
|
Step("wr2", "set_setting", expect: "enabled", value: "true"),
|
||||||
|
Step("wr3", "set_setting", expect: "show_watch_reasons", value: "true"),
|
||||||
|
Step("wr4", "set_setting", expect: "show_dossier_caption", value: "true"),
|
||||||
|
Step("wr5", "spawn", asset: "human"),
|
||||||
|
Step("wr6", "spectator", value: "off"),
|
||||||
|
Step("wr7", "spectator", value: "on"),
|
||||||
|
Step("wr8", "focus", asset: "human"),
|
||||||
|
Step("wr9", "dossier_clear_job"),
|
||||||
|
|
||||||
|
// Fighting label must surface as Fighting - not "Action · King …".
|
||||||
|
Step("wr10", "interest_force_session", asset: "human", label: "Fighting: Isemward", tier: "Action", expect: "fight_reason"),
|
||||||
|
Step("wr11", "assert", expect: "dossier_contains", value: "Fighting"),
|
||||||
|
Step("wr12", "assert", expect: "dossier_not_contains", value: "Action ·"),
|
||||||
|
Step("wr13", "assert", expect: "tip_contains", value: "Fighting"),
|
||||||
|
Step("wr14", "screenshot", value: "hud-watch-reason-fighting.png"),
|
||||||
|
Step("wr15", "wait", wait: 0.55f),
|
||||||
|
|
||||||
|
// Stale identity label must not become "Action · King Name of Realm".
|
||||||
|
Step("wr20", "interest_force_session", asset: "human", label: "King Isemward of Essionan", tier: "Action", expect: "king_identity"),
|
||||||
|
Step("wr21", "assert", expect: "dossier_not_contains", value: "Action ·"),
|
||||||
|
Step("wr22", "assert", expect: "dossier_not_contains", value: "King Isemward"),
|
||||||
|
Step("wr23", "screenshot", value: "hud-watch-reason-identity.png"),
|
||||||
|
Step("wr24", "wait", wait: 0.55f),
|
||||||
|
|
||||||
|
// WorldLog-style event phrase stays clear.
|
||||||
|
Step("wr30", "interest_force_session", asset: "human", label: "War: Essionan vs North", tier: "Epic", expect: "war_reason"),
|
||||||
|
Step("wr31", "assert", expect: "dossier_contains", value: "War"),
|
||||||
|
Step("wr32", "assert", expect: "dossier_not_contains", value: "Action ·"),
|
||||||
|
Step("wr33", "screenshot", value: "hud-watch-reason-war.png"),
|
||||||
|
Step("wr34", "wait", wait: 0.55f),
|
||||||
|
|
||||||
|
Step("wr90", "assert", expect: "no_bad"),
|
||||||
|
Step("wr99", "snapshot"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Actions outrank characters; same action → more important character wins;
|
||||||
|
/// multi-fighter battles outrank anonymous 1v1; notable duels can beat nameless melees.
|
||||||
|
/// </summary>
|
||||||
|
private static List<HarnessCommand> DirectorActionPriority()
|
||||||
|
{
|
||||||
|
return new List<HarnessCommand>
|
||||||
|
{
|
||||||
|
Step("ap0", "dismiss_windows"),
|
||||||
|
Step("ap1", "wait_world"),
|
||||||
|
Step("ap2", "set_setting", expect: "enabled", value: "true"),
|
||||||
|
Step("ap3", "fast_timing", value: "true"),
|
||||||
|
Step("ap4", "spawn", asset: "sheep", count: 2),
|
||||||
|
Step("ap5", "spectator", value: "off"),
|
||||||
|
Step("ap6", "spectator", value: "on"),
|
||||||
|
Step("ap7", "focus", asset: "sheep"),
|
||||||
|
Step("ap8", "interest_variety_clear"),
|
||||||
|
Step("ap9", "interest_expire_pending", value: ""),
|
||||||
|
|
||||||
|
// Same action intensity: higher character significance wins.
|
||||||
|
Step("ap10", "interest_inject", asset: "sheep", label: "FightKing", tier: "Action", expect: "fight_king",
|
||||||
|
value: "lead=event;evt=70;char=40;fighters=2;notables=1"),
|
||||||
|
Step("ap11", "interest_inject", asset: "sheep", label: "FightPeasant", tier: "Action", expect: "fight_peasant",
|
||||||
|
value: "lead=event;evt=70;char=5;fighters=2;notables=0"),
|
||||||
|
Step("ap12", "assert", expect: "interest_score_order", value: "fight_king", label: "fight_peasant"),
|
||||||
|
|
||||||
|
// Multi-person anonymous melee beats anonymous 1v1 at same base evt.
|
||||||
|
Step("ap20", "interest_expire_pending", value: ""),
|
||||||
|
Step("ap21", "interest_inject", asset: "sheep", label: "Melee", tier: "Action", expect: "melee_mass",
|
||||||
|
value: "lead=event;evt=55;char=5;fighters=8;notables=0"),
|
||||||
|
Step("ap22", "interest_inject", asset: "sheep", label: "DuelAnon", tier: "Action", expect: "duel_anon",
|
||||||
|
value: "lead=event;evt=55;char=5;fighters=2;notables=0"),
|
||||||
|
Step("ap23", "assert", expect: "interest_score_order", value: "melee_mass", label: "duel_anon"),
|
||||||
|
|
||||||
|
// Notable duel can beat nameless melee.
|
||||||
|
Step("ap30", "interest_expire_pending", value: ""),
|
||||||
|
Step("ap31", "interest_inject", asset: "sheep", label: "MeleeNoName", tier: "Action", expect: "melee_noname",
|
||||||
|
value: "lead=event;evt=55;char=5;fighters=8;notables=0"),
|
||||||
|
Step("ap32", "interest_inject", asset: "sheep", label: "DuelKings", tier: "Action", expect: "duel_kings",
|
||||||
|
value: "lead=event;evt=55;char=40;fighters=2;notables=2"),
|
||||||
|
Step("ap33", "assert", expect: "interest_score_order", value: "duel_kings", label: "melee_noname"),
|
||||||
|
|
||||||
|
// Combat Action cuts celebrity Story vignette after settle.
|
||||||
|
Step("ap40", "interest_expire_pending", value: ""),
|
||||||
|
Step("ap41", "interest_force_session", asset: "sheep", label: "KingStroll", tier: "Story", expect: "king_stroll"),
|
||||||
|
// Mark current as character vignette (not WorldLog story event).
|
||||||
|
Step("ap42", "interest_inject", asset: "sheep", label: "RealFight", tier: "Action", expect: "real_fight",
|
||||||
|
value: "lead=event;evt=90;char=10;fighters=4;notables=1;force=true"),
|
||||||
|
Step("ap43", "age_current", wait: 1.2f),
|
||||||
|
Step("ap44", "director_run", wait: 1.2f),
|
||||||
|
Step("ap45", "assert", expect: "tip_contains", value: "RealFight"),
|
||||||
|
Step("ap46", "assert", expect: "current_tier", value: "Action"),
|
||||||
|
|
||||||
|
Step("ap90", "assert", expect: "no_bad"),
|
||||||
|
Step("ap91", "fast_timing", value: "false"),
|
||||||
|
Step("ap99", "snapshot"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private static List<HarnessCommand> Discovery()
|
private static List<HarnessCommand> Discovery()
|
||||||
{
|
{
|
||||||
return new List<HarnessCommand>
|
return new List<HarnessCommand>
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,10 @@ public sealed class InterestCandidate
|
||||||
public float Novelty = 1f;
|
public float Novelty = 1f;
|
||||||
public float VisualConfidence = 0.5f;
|
public float VisualConfidence = 0.5f;
|
||||||
public float TotalScore;
|
public float TotalScore;
|
||||||
|
/// <summary>Live fighters / participants in a cluster (battles, multi-unit events).</summary>
|
||||||
|
public int ParticipantCount;
|
||||||
|
/// <summary>Notable (king/favorite/leader/high renown) participants in the cluster.</summary>
|
||||||
|
public int NotableParticipantCount;
|
||||||
public Vector3 Position;
|
public Vector3 Position;
|
||||||
public Actor FollowUnit;
|
public Actor FollowUnit;
|
||||||
public Actor RelatedUnit;
|
public Actor RelatedUnit;
|
||||||
|
|
@ -103,6 +107,8 @@ public sealed class InterestCandidate
|
||||||
Novelty = Novelty,
|
Novelty = Novelty,
|
||||||
VisualConfidence = VisualConfidence,
|
VisualConfidence = VisualConfidence,
|
||||||
TotalScore = TotalScore,
|
TotalScore = TotalScore,
|
||||||
|
ParticipantCount = ParticipantCount,
|
||||||
|
NotableParticipantCount = NotableParticipantCount,
|
||||||
Position = Position,
|
Position = Position,
|
||||||
FollowUnit = FollowUnit,
|
FollowUnit = FollowUnit,
|
||||||
RelatedUnit = RelatedUnit,
|
RelatedUnit = RelatedUnit,
|
||||||
|
|
|
||||||
|
|
@ -621,12 +621,29 @@ public static class InterestDirector
|
||||||
|
|
||||||
if (protectedScene && cur >= InterestTier.Action)
|
if (protectedScene && cur >= InterestTier.Action)
|
||||||
{
|
{
|
||||||
// Action/Story protected: only Epic after settle grace.
|
// Epic always may cut in after settle.
|
||||||
if (next >= InterestTier.Epic && onCurrent >= _settleGrace)
|
if (next >= InterestTier.Epic && onCurrent >= _settleGrace)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Live combat / battles beat celebrity Story vignettes (actions > characters).
|
||||||
|
if (InterestScoring.IsCombatAction(candidate)
|
||||||
|
&& InterestScoring.IsCharacterVignette(_current)
|
||||||
|
&& onCurrent >= _settleGrace)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stronger Action combat may cut a weaker non-combat Action after settle.
|
||||||
|
if (next >= InterestTier.Action
|
||||||
|
&& InterestScoring.IsCombatAction(candidate)
|
||||||
|
&& !InterestScoring.IsCombatAction(_current)
|
||||||
|
&& onCurrent >= _settleGrace)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,9 @@ public sealed class InterestEvent
|
||||||
public string Label;
|
public string Label;
|
||||||
public float CreatedAt;
|
public float CreatedAt;
|
||||||
public string AssetId;
|
public string AssetId;
|
||||||
|
public int ParticipantCount;
|
||||||
|
public int NotableParticipantCount;
|
||||||
|
public float CharacterSignificance;
|
||||||
|
|
||||||
public bool HasFollowUnit => FollowUnit != null && FollowUnit.isAlive();
|
public bool HasFollowUnit => FollowUnit != null && FollowUnit.isAlive();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -172,10 +172,12 @@ public static class InterestFeeds
|
||||||
Key = key,
|
Key = key,
|
||||||
Urgency = interest.Tier,
|
Urgency = interest.Tier,
|
||||||
LeadKind = lead,
|
LeadKind = lead,
|
||||||
Category = CategoryForTier(interest.Tier, asset),
|
Category = asset == "live_battle" ? "Combat" : CategoryForTier(interest.Tier, asset),
|
||||||
Source = "direct",
|
Source = "direct",
|
||||||
EventStrength = interest.Score > 0f ? interest.Score : 40f + (int)interest.Tier * 15f,
|
EventStrength = interest.Score > 0f ? interest.Score : 40f + (int)interest.Tier * 15f,
|
||||||
CharacterSignificance = lead == InterestLeadKind.CharacterLed ? interest.Score : 0f,
|
CharacterSignificance = interest.CharacterSignificance > 0f
|
||||||
|
? interest.CharacterSignificance
|
||||||
|
: (lead == InterestLeadKind.CharacterLed ? interest.Score * 0.5f : 0f),
|
||||||
VisualConfidence = unit != null ? 0.75f : 0.4f,
|
VisualConfidence = unit != null ? 0.75f : 0.4f,
|
||||||
Position = interest.Position,
|
Position = interest.Position,
|
||||||
FollowUnit = unit,
|
FollowUnit = unit,
|
||||||
|
|
@ -183,6 +185,9 @@ public static class InterestFeeds
|
||||||
Label = interest.Label ?? "",
|
Label = interest.Label ?? "",
|
||||||
AssetId = asset,
|
AssetId = asset,
|
||||||
SpeciesId = unit?.asset != null ? unit.asset.id : asset,
|
SpeciesId = unit?.asset != null ? unit.asset.id : asset,
|
||||||
|
ParticipantCount = interest.ParticipantCount,
|
||||||
|
NotableParticipantCount = interest.NotableParticipantCount,
|
||||||
|
Verb = asset == "live_battle" ? "fighting" : "",
|
||||||
CreatedAt = interest.CreatedAt > 0f ? interest.CreatedAt : Time.unscaledTime,
|
CreatedAt = interest.CreatedAt > 0f ? interest.CreatedAt : Time.unscaledTime,
|
||||||
LastSeenAt = Time.unscaledTime,
|
LastSeenAt = Time.unscaledTime,
|
||||||
ExpiresAt = Time.unscaledTime + 30f,
|
ExpiresAt = Time.unscaledTime + 30f,
|
||||||
|
|
|
||||||
|
|
@ -140,25 +140,46 @@ public static class InterestScoring
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
float urgencyWeight = 1f + (int)c.Urgency * 0.15f;
|
// Action-primary: event strength dominates. Character is a tie-break / small amplifier.
|
||||||
if (c.LeadKind == InterestLeadKind.EventLed)
|
// Scale / notables on clusters (battles) add to event strength before this runs.
|
||||||
|
float charWeight = c.LeadKind == InterestLeadKind.EventLed ? 0.18f : 0.55f;
|
||||||
|
float urgencyWeight = 1f + (int)c.Urgency * 0.08f;
|
||||||
|
float scaleBonus = 0f;
|
||||||
|
int fighters = c.ParticipantCount;
|
||||||
|
int notables = c.NotableParticipantCount;
|
||||||
|
if (fighters >= 4)
|
||||||
{
|
{
|
||||||
c.TotalScore = c.EventStrength * urgencyWeight
|
// Multi-person melees outrank thin anonymous 1v1s.
|
||||||
+ c.CharacterSignificance * 0.35f
|
scaleBonus += 28f + Mathf.Min(24f, (fighters - 4) * 4f);
|
||||||
+ c.VisualConfidence * 20f
|
|
||||||
+ c.Novelty * 5f;
|
|
||||||
c.ScoreDetail =
|
|
||||||
$"evt={c.EventStrength:0.#} char={c.CharacterSignificance:0.#} vis={c.VisualConfidence:0.##} nov={c.Novelty:0.##}";
|
|
||||||
}
|
}
|
||||||
else
|
else if (fighters > 0 && fighters <= 2)
|
||||||
{
|
{
|
||||||
c.TotalScore = c.CharacterSignificance * urgencyWeight
|
if (notables >= 2)
|
||||||
+ c.EventStrength * 0.25f
|
{
|
||||||
+ c.VisualConfidence * 12f
|
// Extremely important duel (kings/favorites) can beat nameless melees.
|
||||||
+ c.Novelty * 5f;
|
scaleBonus += 55f;
|
||||||
c.ScoreDetail =
|
}
|
||||||
$"char={c.CharacterSignificance:0.#} evt={c.EventStrength:0.#} vis={c.VisualConfidence:0.##} nov={c.Novelty:0.##}";
|
else if (notables == 1)
|
||||||
|
{
|
||||||
|
scaleBonus += 15f;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
scaleBonus -= 12f;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (notables > 0 && fighters >= 3)
|
||||||
|
{
|
||||||
|
scaleBonus += Mathf.Min(35f, notables * 12f);
|
||||||
|
}
|
||||||
|
|
||||||
|
c.TotalScore = (c.EventStrength + scaleBonus) * urgencyWeight
|
||||||
|
+ c.CharacterSignificance * charWeight
|
||||||
|
+ c.VisualConfidence * 12f
|
||||||
|
+ c.Novelty * 4f;
|
||||||
|
c.ScoreDetail =
|
||||||
|
$"evt={c.EventStrength:0.#} char={c.CharacterSignificance:0.#} n={fighters}/{notables} vis={c.VisualConfidence:0.##}";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static InterestMetadataSnapshot GetOrBuildMeta(Actor actor, float now)
|
public static InterestMetadataSnapshot GetOrBuildMeta(Actor actor, float now)
|
||||||
|
|
@ -308,6 +329,118 @@ public static class InterestScoring
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int kills = actor.data != null ? actor.data.kills : 0;
|
||||||
|
if (kills >= 40 || actor.renown >= 200f)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>King, favorite, or very high renown/kills - can make a 1v1 outrank a no-name melee.</summary>
|
||||||
|
public static bool IsExtremelyNotable(Actor actor)
|
||||||
|
{
|
||||||
|
if (actor == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (actor.isFavorite() || actor.isKing())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int kills = actor.data != null ? actor.data.kills : 0;
|
||||||
|
return kills >= 80 || actor.renown >= 400f;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>True when this candidate is a live fight / battle cluster (not celebrity vignette).</summary>
|
||||||
|
public static bool IsCombatAction(InterestCandidate c)
|
||||||
|
{
|
||||||
|
if (c == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c.AssetId == "live_battle" || c.Category == "Combat")
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(c.Verb)
|
||||||
|
&& c.Verb.IndexOf("fight", System.StringComparison.OrdinalIgnoreCase) >= 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c.Completion == InterestCompletionKind.CombatActive)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c.FollowUnit != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (c.FollowUnit.has_attack_target)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Celebrity / role vignette without a live action beat.</summary>
|
||||||
|
public static bool IsCharacterVignette(InterestCandidate c)
|
||||||
|
{
|
||||||
|
if (c == null || IsCombatAction(c))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c.LeadKind == InterestLeadKind.CharacterLed
|
||||||
|
|| c.Category == "CharacterVignette"
|
||||||
|
|| c.Completion == InterestCompletionKind.CharacterVignette)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scanner/harness "Story" celebrity strolls are not WorldLog events - actions may cut them.
|
||||||
|
if (c.Urgency == InterestTier.Story
|
||||||
|
&& (c.Source == "scanner" || c.Source == "harness")
|
||||||
|
&& string.IsNullOrEmpty(c.HappinessEffectId))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -214,49 +214,231 @@ public sealed class UnitDossier
|
||||||
private static string BuildReasonLine(UnitDossier d, InterestTier? watchTier, string watchLabel, Actor actor,
|
private static string BuildReasonLine(UnitDossier d, InterestTier? watchTier, string watchLabel, Actor actor,
|
||||||
Dictionary<string, int> speciesCounts)
|
Dictionary<string, int> speciesCounts)
|
||||||
{
|
{
|
||||||
string tierPart = watchTier.HasValue ? watchTier.Value.ToString() : "";
|
string why = ResolveWatchWhy(d, watchLabel, actor, speciesCounts);
|
||||||
string why = "";
|
string badge = WatchBadge(watchTier, why);
|
||||||
if (!string.IsNullOrEmpty(HarnessReasonOverride))
|
|
||||||
{
|
|
||||||
why = HarnessReasonOverride.Trim();
|
|
||||||
}
|
|
||||||
else if (!string.IsNullOrEmpty(watchLabel))
|
|
||||||
{
|
|
||||||
why = ShortenWatchLabel(watchLabel, d);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
why = ShortenWatchLabel(
|
|
||||||
WorldActivityScanner.FormatUnitLabelPublic(actor, speciesCounts), d);
|
|
||||||
// Ambient focus: FormatUnitLabel often equals the headline ("Name (species)").
|
|
||||||
if (IsRedundantWithHeadline(why, d))
|
|
||||||
{
|
|
||||||
why = FirstFlavorTag(d);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IsRedundantWithHeadline(why, d) || IsShownTraitName(d, why))
|
|
||||||
{
|
|
||||||
why = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
string watchReason;
|
string watchReason;
|
||||||
if (string.IsNullOrEmpty(tierPart))
|
if (string.IsNullOrEmpty(badge))
|
||||||
{
|
{
|
||||||
watchReason = why ?? "";
|
watchReason = why ?? "";
|
||||||
}
|
}
|
||||||
else if (string.IsNullOrEmpty(why) || why.IndexOf(tierPart, System.StringComparison.OrdinalIgnoreCase) >= 0)
|
else if (string.IsNullOrEmpty(why)
|
||||||
|
|| why.IndexOf(badge, System.StringComparison.OrdinalIgnoreCase) >= 0)
|
||||||
{
|
{
|
||||||
watchReason = IsShownTraitName(d, tierPart) ? "" : tierPart;
|
watchReason = badge;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
watchReason = tierPart + " · " + why;
|
watchReason = badge + " · " + why;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ComposeReasonWithJob(watchReason, d.JobLabel);
|
return ComposeReasonWithJob(watchReason, d.JobLabel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prefer a concrete event/activity phrase over identity ("King Name of X").
|
||||||
|
/// Nametag already shows who; this row should answer why the camera is here.
|
||||||
|
/// </summary>
|
||||||
|
private static string ResolveWatchWhy(
|
||||||
|
UnitDossier d,
|
||||||
|
string watchLabel,
|
||||||
|
Actor actor,
|
||||||
|
Dictionary<string, int> speciesCounts)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(HarnessReasonOverride))
|
||||||
|
{
|
||||||
|
return HarnessReasonOverride.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Live state beats a stale identity label (fighting king, burning favorite, …).
|
||||||
|
if (d.IsFighting)
|
||||||
|
{
|
||||||
|
return "Fighting";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (d.IsSpectacle)
|
||||||
|
{
|
||||||
|
return "Spectacle";
|
||||||
|
}
|
||||||
|
|
||||||
|
InterestCandidate scene = InterestDirector.CurrentCandidate;
|
||||||
|
if (scene != null
|
||||||
|
&& (scene.FollowUnit == actor
|
||||||
|
|| (!string.IsNullOrEmpty(scene.Label)
|
||||||
|
&& actor != null
|
||||||
|
&& scene.SubjectId == d.UnitId)))
|
||||||
|
{
|
||||||
|
string fromScene = WhyFromCandidate(scene, d);
|
||||||
|
if (!string.IsNullOrEmpty(fromScene))
|
||||||
|
{
|
||||||
|
return fromScene;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string fromLabel = ShortenWatchLabel(watchLabel, d);
|
||||||
|
if (!string.IsNullOrEmpty(fromLabel) && !IsIdentityWhy(fromLabel, d))
|
||||||
|
{
|
||||||
|
return fromLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
string flavor = FirstFlavorTag(d);
|
||||||
|
if (!string.IsNullOrEmpty(flavor))
|
||||||
|
{
|
||||||
|
return flavor;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(fromLabel))
|
||||||
|
{
|
||||||
|
return fromLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
string fallback = ShortenWatchLabel(
|
||||||
|
WorldActivityScanner.FormatUnitLabelPublic(actor, speciesCounts), d);
|
||||||
|
if (IsRedundantWithHeadline(fallback, d) || IsIdentityWhy(fallback, d))
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallback ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string WhyFromCandidate(InterestCandidate scene, UnitDossier d)
|
||||||
|
{
|
||||||
|
if (scene == null)
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(scene.HappinessEffectId))
|
||||||
|
{
|
||||||
|
if (scene.HappinessEffectId.StartsWith("death_", System.StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
string grief = ShortenWatchLabel(scene.Label, d);
|
||||||
|
if (!string.IsNullOrEmpty(grief) && !IsIdentityWhy(grief, d))
|
||||||
|
{
|
||||||
|
return grief;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Grief";
|
||||||
|
}
|
||||||
|
|
||||||
|
string happy = ShortenWatchLabel(scene.Label, d);
|
||||||
|
if (!string.IsNullOrEmpty(happy) && !IsIdentityWhy(happy, d))
|
||||||
|
{
|
||||||
|
return happy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(scene.Verb)
|
||||||
|
&& scene.Verb.IndexOf("fight", System.StringComparison.OrdinalIgnoreCase) >= 0)
|
||||||
|
{
|
||||||
|
return "Fighting";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(scene.StatusId))
|
||||||
|
{
|
||||||
|
return Humanize(scene.StatusId);
|
||||||
|
}
|
||||||
|
|
||||||
|
string shortened = ShortenWatchLabel(scene.Label, d);
|
||||||
|
if (!string.IsNullOrEmpty(shortened) && !IsIdentityWhy(shortened, d))
|
||||||
|
{
|
||||||
|
return shortened;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(scene.Category)
|
||||||
|
&& !scene.Category.Equals("CharacterVignette", System.StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& !scene.Category.Equals("Settlement", System.StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return Humanize(scene.Category);
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsIdentityWhy(string text, UnitDossier d)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(text))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
string t = text.Trim();
|
||||||
|
if (t.StartsWith("King ", System.StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| t.StartsWith("King of ", System.StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| t.StartsWith("Leader ", System.StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| t.StartsWith("Leader of ", System.StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| t.StartsWith("Favorite", System.StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pure title leftovers after ShortenWatchLabel ("King", "Leader", "Favorite").
|
||||||
|
if (t.Equals("King", System.StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| t.Equals("Leader", System.StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| t.Equals("Favorite", System.StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (d != null
|
||||||
|
&& !string.IsNullOrEmpty(d.Name)
|
||||||
|
&& t.IndexOf(d.Name, System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||||
|
&& t.IndexOf("Fight", System.StringComparison.OrdinalIgnoreCase) < 0
|
||||||
|
&& t.IndexOf("War", System.StringComparison.OrdinalIgnoreCase) < 0
|
||||||
|
&& t.IndexOf("mourn", System.StringComparison.OrdinalIgnoreCase) < 0
|
||||||
|
&& t.IndexOf("New species", System.StringComparison.OrdinalIgnoreCase) < 0)
|
||||||
|
{
|
||||||
|
// "Isemward (human, 12 kills)" style still useful via kills - keep if kills mentioned.
|
||||||
|
if (t.IndexOf("kill", System.StringComparison.OrdinalIgnoreCase) >= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return IsRedundantWithHeadline(t, d);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Soft urgency badge. Skip when the why already carries the meaning (Fighting, War, …).
|
||||||
|
/// </summary>
|
||||||
|
private static string WatchBadge(InterestTier? watchTier, string why)
|
||||||
|
{
|
||||||
|
if (!watchTier.HasValue)
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
string whySafe = why ?? "";
|
||||||
|
bool whyIsEvent = whySafe.IndexOf("Fight", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||||
|
|| whySafe.IndexOf("War", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||||
|
|| whySafe.IndexOf("mourn", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||||
|
|| whySafe.IndexOf("Grief", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||||
|
|| whySafe.IndexOf("Spectacle", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||||
|
|| whySafe.IndexOf("New species", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||||
|
|| whySafe.IndexOf("In action", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||||
|
|| whySafe.IndexOf("Busy", System.StringComparison.OrdinalIgnoreCase) >= 0;
|
||||||
|
|
||||||
|
switch (watchTier.Value)
|
||||||
|
{
|
||||||
|
case InterestTier.Epic:
|
||||||
|
return whyIsEvent && whySafe.IndexOf("War", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||||
|
? ""
|
||||||
|
: "World";
|
||||||
|
case InterestTier.Story:
|
||||||
|
return whyIsEvent ? "" : "Story";
|
||||||
|
case InterestTier.Action:
|
||||||
|
// "Action · …" was jargon; clear event phrases stand alone.
|
||||||
|
return whyIsEvent ? "" : "Live";
|
||||||
|
case InterestTier.Curiosity:
|
||||||
|
return whyIsEvent ? "" : "Curious";
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Watch reason + job on the dossier row; job alone when reason is empty.</summary>
|
/// <summary>Watch reason + job on the dossier row; job alone when reason is empty.</summary>
|
||||||
private static string ComposeReasonWithJob(string watchReason, string jobLabel)
|
private static string ComposeReasonWithJob(string watchReason, string jobLabel)
|
||||||
{
|
{
|
||||||
|
|
@ -283,6 +465,31 @@ public sealed class UnitDossier
|
||||||
|
|
||||||
private static string FirstFlavorTag(UnitDossier d)
|
private static string FirstFlavorTag(UnitDossier d)
|
||||||
{
|
{
|
||||||
|
// Prefer activity/event tags over identity (king/leader/favorite).
|
||||||
|
string[] prefer =
|
||||||
|
{
|
||||||
|
"fighting", "spectacle", "lone of kind", "warrior",
|
||||||
|
"100+ kills", "50+ kills", "10+ kills"
|
||||||
|
};
|
||||||
|
for (int p = 0; p < prefer.Length; p++)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < d.ScoreReasons.Count; i++)
|
||||||
|
{
|
||||||
|
if (!string.Equals(d.ScoreReasons[i], prefer[p], System.StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
string reason = Humanize(d.ScoreReasons[i]);
|
||||||
|
if (!string.IsNullOrEmpty(reason)
|
||||||
|
&& !IsRedundantWithHeadline(reason, d)
|
||||||
|
&& !IsShownTraitName(d, reason))
|
||||||
|
{
|
||||||
|
return reason;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (int i = 0; i < d.ScoreReasons.Count; i++)
|
for (int i = 0; i < d.ScoreReasons.Count; i++)
|
||||||
{
|
{
|
||||||
string reason = Humanize(d.ScoreReasons[i]);
|
string reason = Humanize(d.ScoreReasons[i]);
|
||||||
|
|
@ -302,6 +509,26 @@ public sealed class UnitDossier
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Identity alone is weak when nametag already shows the unit.
|
||||||
|
if (reason.Equals("king", System.StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| reason.Equals("leader", System.StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| reason.Equals("favorite", System.StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(d.KingdomName)
|
||||||
|
&& reason.Equals("king", System.StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return "King of " + d.KingdomName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(d.CityName)
|
||||||
|
&& reason.Equals("leader", System.StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return "Leader of " + d.CityName;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
return reason;
|
return reason;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -87,25 +87,27 @@ public static class WorldActivityScanner
|
||||||
for (int i = 0; i < n; i++)
|
for (int i = 0; i < n; i++)
|
||||||
{
|
{
|
||||||
Actor actor = alive[i];
|
Actor actor = alive[i];
|
||||||
float score = ScoreActor(actor, speciesCounts);
|
|
||||||
ActivityBand band = ActivityInterestTable.Classify(actor);
|
ActivityBand band = ActivityInterestTable.Classify(actor);
|
||||||
InterestTier tier = InterestTier.Ambient;
|
InterestTier tier = InterestTier.Ambient;
|
||||||
InterestLeadKind lead = InterestLeadKind.CharacterLed;
|
InterestLeadKind lead = InterestLeadKind.CharacterLed;
|
||||||
|
// Actions outrank characters: combat/hot work are Action events; kings walking stay vignettes.
|
||||||
if (band == ActivityBand.Hot || actor.has_attack_target)
|
if (band == ActivityBand.Hot || actor.has_attack_target)
|
||||||
{
|
{
|
||||||
tier = InterestTier.Action;
|
tier = InterestTier.Action;
|
||||||
lead = InterestLeadKind.EventLed;
|
lead = InterestLeadKind.EventLed;
|
||||||
}
|
}
|
||||||
else if (band >= ActivityBand.Warm && (actor.isFavorite() || actor.isKing() || actor.isCityLeader()))
|
else if (band >= ActivityBand.Warm)
|
||||||
{
|
{
|
||||||
tier = InterestTier.Story;
|
tier = InterestTier.Action;
|
||||||
lead = InterestLeadKind.EventLed;
|
lead = InterestLeadKind.EventLed;
|
||||||
}
|
}
|
||||||
else if (IsSpectacle(actor) || IsSingletonSpecies(actor, speciesCounts))
|
else if (IsSpectacle(actor) || IsSingletonSpecies(actor, speciesCounts))
|
||||||
{
|
{
|
||||||
tier = InterestTier.Curiosity;
|
tier = InterestTier.Curiosity;
|
||||||
|
lead = InterestLeadKind.CharacterLed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SplitActorScore(actor, speciesCounts, out float actionScore, out float charScore);
|
||||||
long id = 0;
|
long id = 0;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -120,32 +122,58 @@ public static class WorldActivityScanner
|
||||||
? "vignette:" + id + ":" + (actor.asset != null ? actor.asset.id : "unit")
|
? "vignette:" + id + ":" + (actor.asset != null ? actor.asset.id : "unit")
|
||||||
: "scan:" + tier + ":" + id;
|
: "scan:" + tier + ":" + id;
|
||||||
|
|
||||||
|
string label = FormatUnitLabel(actor, speciesCounts);
|
||||||
|
if (actor.has_attack_target)
|
||||||
|
{
|
||||||
|
label = "Fighting: " + SafeName(actor);
|
||||||
|
}
|
||||||
|
else if (band == ActivityBand.Hot)
|
||||||
|
{
|
||||||
|
label = "In action";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool combat = actor.has_attack_target || band == ActivityBand.Hot;
|
||||||
|
int fighters = 1;
|
||||||
|
int notables = InterestScoring.IsNotable(actor) ? 1 : 0;
|
||||||
|
if (combat)
|
||||||
|
{
|
||||||
|
CountFightCluster(actor.current_position, 10f, out fighters, out notables, out _);
|
||||||
|
fighters = Mathf.Max(1, fighters);
|
||||||
|
}
|
||||||
|
|
||||||
var candidate = new InterestCandidate
|
var candidate = new InterestCandidate
|
||||||
{
|
{
|
||||||
Key = key,
|
Key = key,
|
||||||
Urgency = tier,
|
Urgency = tier,
|
||||||
LeadKind = lead,
|
LeadKind = lead,
|
||||||
Category = lead == InterestLeadKind.CharacterLed ? "CharacterVignette" : "Combat",
|
Category = combat
|
||||||
|
? "Combat"
|
||||||
|
: (lead == InterestLeadKind.CharacterLed ? "CharacterVignette" : "Work"),
|
||||||
Source = "scanner",
|
Source = "scanner",
|
||||||
EventStrength = lead == InterestLeadKind.EventLed ? score : score * 0.3f,
|
EventStrength = lead == InterestLeadKind.EventLed ? actionScore : actionScore * 0.35f,
|
||||||
CharacterSignificance = score,
|
CharacterSignificance = charScore,
|
||||||
VisualConfidence = band >= ActivityBand.Warm ? 0.7f : 0.4f,
|
VisualConfidence = band >= ActivityBand.Warm ? 0.7f : 0.4f,
|
||||||
Position = actor.current_position,
|
Position = actor.current_position,
|
||||||
FollowUnit = actor,
|
FollowUnit = actor,
|
||||||
SubjectId = id,
|
SubjectId = id,
|
||||||
Label = FormatUnitLabel(actor, speciesCounts),
|
Label = label,
|
||||||
|
Verb = actor.has_attack_target ? "fighting" : (band == ActivityBand.Hot ? "busy" : ""),
|
||||||
AssetId = actor.asset != null ? actor.asset.id : "scored_unit",
|
AssetId = actor.asset != null ? actor.asset.id : "scored_unit",
|
||||||
SpeciesId = actor.asset != null ? actor.asset.id : "",
|
SpeciesId = actor.asset != null ? actor.asset.id : "",
|
||||||
CityKey = actor.city != null ? (actor.city.name ?? "") : "",
|
CityKey = actor.city != null ? (actor.city.name ?? "") : "",
|
||||||
KingdomKey = actor.kingdom != null ? (actor.kingdom.name ?? "") : "",
|
KingdomKey = actor.kingdom != null ? (actor.kingdom.name ?? "") : "",
|
||||||
|
ParticipantCount = combat ? fighters : 0,
|
||||||
|
NotableParticipantCount = combat ? notables : 0,
|
||||||
CreatedAt = Time.unscaledTime,
|
CreatedAt = Time.unscaledTime,
|
||||||
LastSeenAt = Time.unscaledTime,
|
LastSeenAt = Time.unscaledTime,
|
||||||
ExpiresAt = Time.unscaledTime + 18f,
|
ExpiresAt = Time.unscaledTime + 18f,
|
||||||
MinWatch = 2f,
|
MinWatch = 2f,
|
||||||
MaxWatch = 22f,
|
MaxWatch = 22f,
|
||||||
Completion = lead == InterestLeadKind.EventLed
|
Completion = combat
|
||||||
? InterestCompletionKind.CombatActive
|
? InterestCompletionKind.CombatActive
|
||||||
: InterestCompletionKind.CharacterVignette
|
: (lead == InterestLeadKind.EventLed
|
||||||
|
? InterestCompletionKind.ActivityActive
|
||||||
|
: InterestCompletionKind.CharacterVignette)
|
||||||
};
|
};
|
||||||
InterestScoring.ScoreCheap(candidate);
|
InterestScoring.ScoreCheap(candidate);
|
||||||
InterestRegistry.Upsert(candidate);
|
InterestRegistry.Upsert(candidate);
|
||||||
|
|
@ -167,15 +195,38 @@ public static class WorldActivityScanner
|
||||||
{
|
{
|
||||||
_cachedBattle = BuildHottestBattle();
|
_cachedBattle = BuildHottestBattle();
|
||||||
InterestEvent bestUnit = BuildBestScoredUnit();
|
InterestEvent bestUnit = BuildBestScoredUnit();
|
||||||
|
_cachedBest = PreferRicherAction(_cachedBattle, bestUnit);
|
||||||
|
}
|
||||||
|
|
||||||
// Prefer an active battle when it is meaningfully hotter than quiet unit watching.
|
/// <summary>
|
||||||
if (_cachedBattle != null)
|
/// Pick battle vs unit dynamically: mass fights win unless a notable 1v1 outranks an anonymous melee.
|
||||||
|
/// </summary>
|
||||||
|
private static InterestEvent PreferRicherAction(InterestEvent battle, InterestEvent unit)
|
||||||
|
{
|
||||||
|
if (battle == null)
|
||||||
{
|
{
|
||||||
_cachedBest = _cachedBattle;
|
return unit;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_cachedBest = bestUnit;
|
if (unit == null)
|
||||||
|
{
|
||||||
|
return battle;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool unitDuel = unit.FollowUnit != null
|
||||||
|
&& unit.FollowUnit.has_attack_target
|
||||||
|
&& unit.ParticipantCount > 0
|
||||||
|
&& unit.ParticipantCount <= 2
|
||||||
|
&& unit.NotableParticipantCount >= 2;
|
||||||
|
bool battleAnonymousMelee = battle.ParticipantCount >= 4
|
||||||
|
&& battle.NotableParticipantCount == 0;
|
||||||
|
|
||||||
|
if (unitDuel && battleAnonymousMelee && unit.Score + 15f >= battle.Score)
|
||||||
|
{
|
||||||
|
return unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
return battle.Score >= unit.Score ? battle : unit;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static InterestEvent BuildHottestBattle()
|
private static InterestEvent BuildHottestBattle()
|
||||||
|
|
@ -183,11 +234,12 @@ public static class WorldActivityScanner
|
||||||
HashSet<BattleContainer> battles = BattleKeeperManager.get();
|
HashSet<BattleContainer> battles = BattleKeeperManager.get();
|
||||||
if (battles == null || battles.Count == 0)
|
if (battles == null || battles.Count == 0)
|
||||||
{
|
{
|
||||||
return null;
|
// Fall through to live fight clusters with no BattleKeeper entry yet.
|
||||||
|
return BuildHottestFightCluster();
|
||||||
}
|
}
|
||||||
|
|
||||||
BattleContainer best = null;
|
InterestEvent best = null;
|
||||||
int bestDeaths = 0;
|
float bestScore = float.MinValue;
|
||||||
foreach (BattleContainer battle in battles)
|
foreach (BattleContainer battle in battles)
|
||||||
{
|
{
|
||||||
if (battle?.tile == null)
|
if (battle?.tile == null)
|
||||||
|
|
@ -196,30 +248,213 @@ public static class WorldActivityScanner
|
||||||
}
|
}
|
||||||
|
|
||||||
int deaths = battle.getDeathsTotal();
|
int deaths = battle.getDeathsTotal();
|
||||||
if (deaths > bestDeaths)
|
Vector3 pos = battle.tile.posV3;
|
||||||
|
CountFightCluster(pos, 14f, out int fighters, out int notables, out Actor follow);
|
||||||
|
if (deaths < 1 && fighters < 2)
|
||||||
{
|
{
|
||||||
bestDeaths = deaths;
|
continue;
|
||||||
best = battle;
|
}
|
||||||
|
|
||||||
|
fighters = Mathf.Max(fighters, deaths > 0 ? 2 : 0);
|
||||||
|
float score = ScoreFightCluster(deaths, fighters, notables);
|
||||||
|
if (score > bestScore)
|
||||||
|
{
|
||||||
|
bestScore = score;
|
||||||
|
string label = fighters >= 5 || deaths >= 6
|
||||||
|
? $"Battle ({fighters} fighting, {deaths} fallen)"
|
||||||
|
: fighters <= 2
|
||||||
|
? $"Duel ({deaths} fallen)"
|
||||||
|
: $"Skirmish ({fighters} fighting)";
|
||||||
|
best = new InterestEvent
|
||||||
|
{
|
||||||
|
Tier = InterestTier.Action,
|
||||||
|
Score = score,
|
||||||
|
Position = pos,
|
||||||
|
FollowUnit = follow ?? FindNearestAliveUnit(pos, 14f),
|
||||||
|
Label = label,
|
||||||
|
CreatedAt = Time.unscaledTime,
|
||||||
|
AssetId = "live_battle",
|
||||||
|
ParticipantCount = fighters,
|
||||||
|
NotableParticipantCount = notables,
|
||||||
|
CharacterSignificance = notables * 18f
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (best == null || bestDeaths < 1)
|
InterestEvent live = BuildHottestFightCluster();
|
||||||
|
return PreferRicherAction(best, live);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Cluster of units currently in combat (even before BattleKeeper records deaths).</summary>
|
||||||
|
private static InterestEvent BuildHottestFightCluster()
|
||||||
|
{
|
||||||
|
InterestEvent best = null;
|
||||||
|
float bestScore = float.MinValue;
|
||||||
|
var seen = new HashSet<long>();
|
||||||
|
foreach (Actor actor in EnumerateAliveUnits())
|
||||||
{
|
{
|
||||||
return null;
|
if (actor == null || !actor.isAlive() || !actor.has_attack_target)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
long id = 0;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
id = actor.getID();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seen.Contains(id))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
CountFightCluster(actor.current_position, 10f, out int fighters, out int notables, out Actor follow);
|
||||||
|
if (fighters < 1)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark cluster members so we don't emit N duplicates.
|
||||||
|
MarkClusterSeen(actor.current_position, 10f, seen);
|
||||||
|
float score = ScoreFightCluster(deaths: 0, fighters, notables);
|
||||||
|
if (score > bestScore)
|
||||||
|
{
|
||||||
|
bestScore = score;
|
||||||
|
best = new InterestEvent
|
||||||
|
{
|
||||||
|
Tier = InterestTier.Action,
|
||||||
|
Score = score,
|
||||||
|
Position = actor.current_position,
|
||||||
|
FollowUnit = follow ?? actor,
|
||||||
|
Label = fighters <= 2 ? "Duel" : $"Fight ({fighters})",
|
||||||
|
CreatedAt = Time.unscaledTime,
|
||||||
|
AssetId = "live_battle",
|
||||||
|
ParticipantCount = fighters,
|
||||||
|
NotableParticipantCount = notables,
|
||||||
|
CharacterSignificance = notables * 18f
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Vector3 pos = best.tile.posV3;
|
return best;
|
||||||
Actor nearby = FindNearestAliveUnit(pos, 14f);
|
}
|
||||||
return new InterestEvent
|
|
||||||
|
private static float ScoreFightCluster(int deaths, int fighters, int notables)
|
||||||
|
{
|
||||||
|
float score = deaths * 10f + Mathf.Max(0, fighters) * 14f + notables * 22f;
|
||||||
|
// Multi-person melees get a clear bump over thin 1v1s.
|
||||||
|
if (fighters >= 4)
|
||||||
{
|
{
|
||||||
Tier = InterestTier.Action,
|
score += 28f;
|
||||||
Score = 50f + bestDeaths,
|
}
|
||||||
Position = pos,
|
else if (fighters <= 2)
|
||||||
FollowUnit = nearby,
|
{
|
||||||
Label = bestDeaths >= 6 ? $"Battle ({bestDeaths} fallen)" : $"Skirmish ({bestDeaths} fallen)",
|
score -= 8f;
|
||||||
CreatedAt = Time.unscaledTime,
|
// Notable duels recover - king vs king / favorites.
|
||||||
AssetId = "live_battle"
|
if (notables >= 2)
|
||||||
};
|
{
|
||||||
|
score += 45f;
|
||||||
|
}
|
||||||
|
else if (notables == 1)
|
||||||
|
{
|
||||||
|
score += 12f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CountFightCluster(
|
||||||
|
Vector2 pos,
|
||||||
|
float radius,
|
||||||
|
out int fighters,
|
||||||
|
out int notables,
|
||||||
|
out Actor bestFollow)
|
||||||
|
{
|
||||||
|
fighters = 0;
|
||||||
|
notables = 0;
|
||||||
|
bestFollow = null;
|
||||||
|
float bestChar = -1f;
|
||||||
|
float r2 = radius * radius;
|
||||||
|
foreach (Actor actor in EnumerateAliveUnits())
|
||||||
|
{
|
||||||
|
if (actor == null || !actor.isAlive())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
float dx = actor.current_position.x - pos.x;
|
||||||
|
float dy = actor.current_position.y - pos.y;
|
||||||
|
if (dx * dx + dy * dy > r2)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool fighting = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
fighting = actor.has_attack_target
|
||||||
|
|| (actor.hasTask() && actor.ai?.task != null && actor.ai.task.in_combat);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
fighting = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fighting)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
fighters++;
|
||||||
|
bool notable = InterestScoring.IsNotable(actor);
|
||||||
|
bool extreme = InterestScoring.IsExtremelyNotable(actor);
|
||||||
|
if (notable)
|
||||||
|
{
|
||||||
|
notables++;
|
||||||
|
}
|
||||||
|
|
||||||
|
SplitActorScore(actor, null, out _, out float charScore);
|
||||||
|
float weight = charScore + (extreme ? 30f : 0f);
|
||||||
|
if (weight > bestChar)
|
||||||
|
{
|
||||||
|
bestChar = weight;
|
||||||
|
bestFollow = actor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void MarkClusterSeen(Vector2 pos, float radius, HashSet<long> seen)
|
||||||
|
{
|
||||||
|
float r2 = radius * radius;
|
||||||
|
foreach (Actor actor in EnumerateAliveUnits())
|
||||||
|
{
|
||||||
|
if (actor == null || !actor.isAlive() || !actor.has_attack_target)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
float dx = actor.current_position.x - pos.x;
|
||||||
|
float dy = actor.current_position.y - pos.y;
|
||||||
|
if (dx * dx + dy * dy > r2)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
seen.Add(actor.getID());
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static InterestEvent BuildBestScoredUnit()
|
private static InterestEvent BuildBestScoredUnit()
|
||||||
|
|
@ -251,15 +486,15 @@ public static class WorldActivityScanner
|
||||||
for (int i = 0; i < alive.Count; i++)
|
for (int i = 0; i < alive.Count; i++)
|
||||||
{
|
{
|
||||||
Actor actor = alive[i];
|
Actor actor = alive[i];
|
||||||
float score = ScoreActor(actor, speciesCounts);
|
float unitScore = ScoreActor(actor, speciesCounts);
|
||||||
if (score > bestScore + 0.01f)
|
if (unitScore > bestScore + 0.01f)
|
||||||
{
|
{
|
||||||
bestScore = score;
|
bestScore = unitScore;
|
||||||
best = actor;
|
best = actor;
|
||||||
nearTies.Clear();
|
nearTies.Clear();
|
||||||
nearTies.Add(actor);
|
nearTies.Add(actor);
|
||||||
}
|
}
|
||||||
else if (Mathf.Abs(score - bestScore) <= 2f)
|
else if (Mathf.Abs(unitScore - bestScore) <= 2f)
|
||||||
{
|
{
|
||||||
nearTies.Add(actor);
|
nearTies.Add(actor);
|
||||||
}
|
}
|
||||||
|
|
@ -278,17 +513,19 @@ public static class WorldActivityScanner
|
||||||
|
|
||||||
InterestTier tier = InterestTier.Ambient;
|
InterestTier tier = InterestTier.Ambient;
|
||||||
ActivityBand band = ActivityInterestTable.Classify(best);
|
ActivityBand band = ActivityInterestTable.Classify(best);
|
||||||
bool important = best.isFavorite() || best.isKing() || best.isCityLeader();
|
SplitActorScore(best, speciesCounts, out float actionScore, out float charScore);
|
||||||
|
int fighters = 0;
|
||||||
if (band == ActivityBand.Hot || best.has_attack_target
|
int notables = 0;
|
||||||
|| (best.data != null && best.data.kills >= 10 && band >= ActivityBand.Warm))
|
if (best.has_attack_target || band == ActivityBand.Hot)
|
||||||
{
|
{
|
||||||
|
CountFightCluster(best.current_position, 10f, out fighters, out notables, out _);
|
||||||
|
fighters = Mathf.Max(1, fighters);
|
||||||
tier = InterestTier.Action;
|
tier = InterestTier.Action;
|
||||||
}
|
}
|
||||||
else if (band >= ActivityBand.Warm && important)
|
else if (band >= ActivityBand.Warm)
|
||||||
{
|
{
|
||||||
// Important + interesting (not idle king/favorite).
|
// Warm work is an action beat - not Story celebrity.
|
||||||
tier = InterestTier.Story;
|
tier = InterestTier.Action;
|
||||||
}
|
}
|
||||||
else if (IsSpectacle(best) || IsSingletonSpecies(best, speciesCounts))
|
else if (IsSpectacle(best) || IsSingletonSpecies(best, speciesCounts))
|
||||||
{
|
{
|
||||||
|
|
@ -299,15 +536,28 @@ public static class WorldActivityScanner
|
||||||
tier = InterestTier.Ambient;
|
tier = InterestTier.Ambient;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float finalScore = ScoreFightCluster(0, fighters, notables);
|
||||||
|
if (fighters <= 0)
|
||||||
|
{
|
||||||
|
finalScore = actionScore + charScore * 0.18f;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
finalScore = Mathf.Max(finalScore, actionScore + charScore * 0.18f);
|
||||||
|
}
|
||||||
|
|
||||||
return new InterestEvent
|
return new InterestEvent
|
||||||
{
|
{
|
||||||
Tier = tier,
|
Tier = tier,
|
||||||
Score = bestScore,
|
Score = finalScore,
|
||||||
Position = best.current_position,
|
Position = best.current_position,
|
||||||
FollowUnit = best,
|
FollowUnit = best,
|
||||||
Label = FormatUnitLabel(best, speciesCounts),
|
Label = FormatUnitLabel(best, speciesCounts),
|
||||||
CreatedAt = Time.unscaledTime,
|
CreatedAt = Time.unscaledTime,
|
||||||
AssetId = best.asset != null ? best.asset.id : "scored_unit"
|
AssetId = best.asset != null ? best.asset.id : "scored_unit",
|
||||||
|
ParticipantCount = fighters,
|
||||||
|
NotableParticipantCount = notables,
|
||||||
|
CharacterSignificance = charScore
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -390,36 +640,73 @@ public static class WorldActivityScanner
|
||||||
|
|
||||||
private static float ScoreActor(Actor actor, Dictionary<string, int> speciesCounts)
|
private static float ScoreActor(Actor actor, Dictionary<string, int> speciesCounts)
|
||||||
{
|
{
|
||||||
|
SplitActorScore(actor, speciesCounts, out float action, out float character);
|
||||||
|
// Action-primary total used for ranking; character is a light tie-break.
|
||||||
|
return action + character * 0.18f;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Split live action intensity from who the unit is.
|
||||||
|
/// Same action → higher character wins; character alone never dominates a real fight.
|
||||||
|
/// </summary>
|
||||||
|
private static void SplitActorScore(
|
||||||
|
Actor actor,
|
||||||
|
Dictionary<string, int> speciesCounts,
|
||||||
|
out float actionScore,
|
||||||
|
out float characterScore)
|
||||||
|
{
|
||||||
|
actionScore = 0f;
|
||||||
|
characterScore = 0f;
|
||||||
if (actor == null)
|
if (actor == null)
|
||||||
{
|
{
|
||||||
return 0f;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
float activity = ActivityInterestTable.ScoreActivity(actor);
|
float activity = ActivityInterestTable.ScoreActivity(actor);
|
||||||
ActivityBand band = ActivityInterestTable.Classify(actor);
|
ActivityBand band = ActivityInterestTable.Classify(actor);
|
||||||
|
actionScore = activity;
|
||||||
float importance = 0f;
|
if (actor.has_attack_target)
|
||||||
if (actor.isFavorite())
|
|
||||||
{
|
{
|
||||||
importance += 22f;
|
actionScore += 40f;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (actor.isKing())
|
if (band == ActivityBand.Hot)
|
||||||
{
|
{
|
||||||
importance += 18f;
|
actionScore += 18f;
|
||||||
}
|
}
|
||||||
else if (actor.isCityLeader())
|
else if (band == ActivityBand.Warm)
|
||||||
{
|
{
|
||||||
importance += 14f;
|
actionScore += 8f;
|
||||||
}
|
|
||||||
else if (actor.is_profession_warrior)
|
|
||||||
{
|
|
||||||
importance += 6f;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IsSpectacle(actor))
|
if (IsSpectacle(actor))
|
||||||
{
|
{
|
||||||
importance += 8f;
|
actionScore += 12f;
|
||||||
|
}
|
||||||
|
|
||||||
|
int kills = actor.data != null ? actor.data.kills : 0;
|
||||||
|
if (band >= ActivityBand.Warm || actor.has_attack_target)
|
||||||
|
{
|
||||||
|
actionScore += Mathf.Min(10f, kills * 0.2f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Character significance - tie-break / amplifier only.
|
||||||
|
if (actor.isFavorite())
|
||||||
|
{
|
||||||
|
characterScore += 22f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actor.isKing())
|
||||||
|
{
|
||||||
|
characterScore += 18f;
|
||||||
|
}
|
||||||
|
else if (actor.isCityLeader())
|
||||||
|
{
|
||||||
|
characterScore += 14f;
|
||||||
|
}
|
||||||
|
else if (actor.is_profession_warrior)
|
||||||
|
{
|
||||||
|
characterScore += 6f;
|
||||||
}
|
}
|
||||||
|
|
||||||
string speciesId = actor.asset != null ? actor.asset.id : "unknown";
|
string speciesId = actor.asset != null ? actor.asset.id : "unknown";
|
||||||
|
|
@ -427,47 +714,20 @@ public static class WorldActivityScanner
|
||||||
&& speciesCounts.TryGetValue(speciesId, out int pop)
|
&& speciesCounts.TryGetValue(speciesId, out int pop)
|
||||||
&& pop == 1)
|
&& pop == 1)
|
||||||
{
|
{
|
||||||
importance += 10f;
|
characterScore += 8f;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (actor.subspecies != null)
|
characterScore += Mathf.Min(8f, actor.renown / 120f);
|
||||||
|
characterScore += Mathf.Min(8f, kills * 0.15f);
|
||||||
|
|
||||||
|
// Cold kings barely score - do not let identity monopolize the camera.
|
||||||
|
if (band == ActivityBand.Cold && !actor.has_attack_target && !IsSpectacle(actor))
|
||||||
{
|
{
|
||||||
try
|
actionScore *= 0.25f;
|
||||||
{
|
characterScore *= 0.35f;
|
||||||
int subCount = CountSubspeciesMembers(actor.subspecies);
|
|
||||||
if (subCount > 0 && subCount <= 3)
|
|
||||||
{
|
|
||||||
importance += 6f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
importance += Mathf.Min(8f, actor.renown / 120f);
|
actionScore += (actor.getID() % 7) * 0.01f;
|
||||||
|
|
||||||
// Importance only amplifies when the unit is actually doing something.
|
|
||||||
float score;
|
|
||||||
if (band >= ActivityBand.Warm || activity >= ActivityInterestTable.WarmScoreFloor)
|
|
||||||
{
|
|
||||||
score = activity + importance * 0.85f;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Cold: tiny identity bump so favorites/kings do not monopolize Ambient.
|
|
||||||
score = activity + importance * 0.08f;
|
|
||||||
}
|
|
||||||
|
|
||||||
int kills = actor.data != null ? actor.data.kills : 0;
|
|
||||||
if (band >= ActivityBand.Warm)
|
|
||||||
{
|
|
||||||
score += Mathf.Min(12f, kills * 0.35f);
|
|
||||||
}
|
|
||||||
|
|
||||||
score += (actor.getID() % 7) * 0.01f;
|
|
||||||
return score;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>True when the focused unit's current activity is Cold (idle scoring).</summary>
|
/// <summary>True when the focused unit's current activity is Cold (idle scoring).</summary>
|
||||||
|
|
@ -540,23 +800,8 @@ public static class WorldActivityScanner
|
||||||
{
|
{
|
||||||
string name = SafeName(actor);
|
string name = SafeName(actor);
|
||||||
string species = actor.asset != null ? actor.asset.id : "creature";
|
string species = actor.asset != null ? actor.asset.id : "creature";
|
||||||
if (actor.isFavorite())
|
|
||||||
{
|
|
||||||
return $"Favorite: {name}";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (actor.isKing())
|
|
||||||
{
|
|
||||||
string kingdom = actor.kingdom != null ? actor.kingdom.name : "realm";
|
|
||||||
return $"King {name} of {kingdom}";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (actor.isCityLeader())
|
|
||||||
{
|
|
||||||
string city = actor.city != null ? actor.city.name : "town";
|
|
||||||
return $"Leader {name} of {city}";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Event / activity first - titles alone are not a watch reason (nametag already has the name).
|
||||||
if (actor.has_attack_target)
|
if (actor.has_attack_target)
|
||||||
{
|
{
|
||||||
return $"Fighting: {name}";
|
return $"Fighting: {name}";
|
||||||
|
|
@ -567,6 +812,23 @@ public static class WorldActivityScanner
|
||||||
return $"Spectacle: {name} ({species})";
|
return $"Spectacle: {name} ({species})";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (actor.isFavorite())
|
||||||
|
{
|
||||||
|
return $"Favorite: {name}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actor.isKing())
|
||||||
|
{
|
||||||
|
string kingdom = actor.kingdom != null ? actor.kingdom.name : "realm";
|
||||||
|
return $"King of {kingdom}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actor.isCityLeader())
|
||||||
|
{
|
||||||
|
string city = actor.city != null ? actor.city.name : "town";
|
||||||
|
return $"Leader of {city}";
|
||||||
|
}
|
||||||
|
|
||||||
if (IsSingletonSpecies(actor, speciesCounts))
|
if (IsSingletonSpecies(actor, speciesCounts))
|
||||||
{
|
{
|
||||||
return $"Lone {species}: {name}";
|
return $"Lone {species}: {name}";
|
||||||
|
|
@ -779,7 +1041,10 @@ public static class WorldActivityScanner
|
||||||
FollowUnit = source.FollowUnit,
|
FollowUnit = source.FollowUnit,
|
||||||
Label = source.Label,
|
Label = source.Label,
|
||||||
CreatedAt = Time.unscaledTime,
|
CreatedAt = Time.unscaledTime,
|
||||||
AssetId = source.AssetId
|
AssetId = source.AssetId,
|
||||||
|
ParticipantCount = source.ParticipantCount,
|
||||||
|
NotableParticipantCount = source.NotableParticipantCount,
|
||||||
|
CharacterSignificance = source.CharacterSignificance
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "IdleSpectator",
|
"name": "IdleSpectator",
|
||||||
"author": "dazed",
|
"author": "dazed",
|
||||||
"version": "0.14.16",
|
"version": "0.14.21",
|
||||||
"description": "AFK Idle Spectator (I) + Lore (L). Event-centered scene director + happiness Activity/Life logging.",
|
"description": "AFK Idle Spectator (I) + Lore (L). Event-centered scene director + happiness Activity/Life logging.",
|
||||||
"GUID": "com.dazed.idlespectator"
|
"GUID": "com.dazed.idlespectator"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ REGRESSION_SCENARIOS=(
|
||||||
director_lifecycle
|
director_lifecycle
|
||||||
interest_happiness
|
interest_happiness
|
||||||
director_gaps
|
director_gaps
|
||||||
|
director_action_priority
|
||||||
discovery
|
discovery
|
||||||
world_log
|
world_log
|
||||||
chronicle_smoke
|
chronicle_smoke
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue