Enhance theater lead handling and update combat scenarios.
- Introduce theater lead management to maintain focus during combat - Add new steps for combat scenarios involving spectacle creatures - Update InterestCandidate to track theater lead and combat participation - Refactor InterestDirector to improve focus handling across maintain ticks - Increment version to 0.28.18 in mod.json
This commit is contained in:
parent
03729664be
commit
04d5bcb381
6 changed files with 452 additions and 61 deletions
10
.tmp-logs/README.md
Normal file
10
.tmp-logs/README.md
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
# Saved Player.log archives
|
||||||
|
|
||||||
|
## Player-pre-theater-lead-20260717-172418.log
|
||||||
|
|
||||||
|
Captured before the Mass/Battle theater-lead focus hold fix (mod 0.28.17 soak).
|
||||||
|
|
||||||
|
Use this to evaluate whether the director was showing interesting events overall,
|
||||||
|
separate from the combat camera thrash that was fixed in 0.28.18.
|
||||||
|
|
||||||
|
Key stretch: Evil Mage vs The Godo humans (mass fight + duel partner churn).
|
||||||
|
|
@ -1144,6 +1144,35 @@ internal static class HarnessScenarios
|
||||||
Step("cf45", "assert", expect: "tip_matches_focus"),
|
Step("cf45", "assert", expect: "tip_matches_focus"),
|
||||||
Step("cf46", "assert", expect: "dossier_matches_focus"),
|
Step("cf46", "assert", expect: "dossier_matches_focus"),
|
||||||
Step("cf47", "assert", expect: "has_focus", value: "true"),
|
Step("cf47", "assert", expect: "has_focus", value: "true"),
|
||||||
|
// Theater lead: collective maintain must not thrash Follow across the mob.
|
||||||
|
Step("cf47a", "remember_focus"),
|
||||||
|
Step("cf47b", "combat_maintain_focus"),
|
||||||
|
Step("cf47c", "wait", value: "0.35"),
|
||||||
|
Step("cf47d", "combat_maintain_focus"),
|
||||||
|
Step("cf47e", "wait", value: "0.35"),
|
||||||
|
Step("cf47f", "combat_maintain_focus"),
|
||||||
|
Step("cf47g", "assert", expect: "focus_same"),
|
||||||
|
|
||||||
|
// Spectacle theater lead: evil mage vs human mob holds the mage, not a random human.
|
||||||
|
Step("cf48", "interest_end_session"),
|
||||||
|
Step("cf48b", "spawn", asset: "evil_mage", count: 1),
|
||||||
|
Step("cf48c", "spawn", asset: "human", count: 1),
|
||||||
|
Step("cf48d", "pick_unit", asset: "evil_mage"),
|
||||||
|
Step("cf48e", "interest_combat_session", asset: "evil_mage", value: "human", expect: "cf_mage"),
|
||||||
|
Step("cf48f", "combat_ensemble_escalate", value: "8"),
|
||||||
|
Step("cf48g", "combat_wire_attack_sides", asset: "evil_mage", value: "human"),
|
||||||
|
Step("cf48h", "wait", value: "0.35"),
|
||||||
|
Step("cf48i", "combat_maintain_focus"),
|
||||||
|
Step("cf48j", "assert", expect: "unit_asset", asset: "evil_mage"),
|
||||||
|
Step("cf48k", "remember_focus"),
|
||||||
|
Step("cf48l", "combat_maintain_focus"),
|
||||||
|
Step("cf48m", "wait", value: "0.35"),
|
||||||
|
Step("cf48n", "combat_maintain_focus"),
|
||||||
|
Step("cf48o", "wait", value: "0.35"),
|
||||||
|
Step("cf48p", "combat_maintain_focus"),
|
||||||
|
Step("cf48q", "assert", expect: "focus_same"),
|
||||||
|
Step("cf48r", "assert", expect: "unit_asset", asset: "evil_mage"),
|
||||||
|
Step("cf48s", "assert", expect: "tip_matches_any", value: "Skirmish -|Battle -|Mass -"),
|
||||||
|
|
||||||
// Natural Duel → multi: seed packs without tip rewrite; maintain must escalate.
|
// Natural Duel → multi: seed packs without tip rewrite; maintain must escalate.
|
||||||
Step("cf50", "interest_end_session"),
|
Step("cf50", "interest_end_session"),
|
||||||
|
|
@ -1226,6 +1255,14 @@ internal static class HarnessScenarios
|
||||||
Step("csl45b", "assert", expect: "tip_not_contains", value: "melee"),
|
Step("csl45b", "assert", expect: "tip_not_contains", value: "melee"),
|
||||||
Step("csl46", "assert", expect: "unit_asset", value: "human|wolf"),
|
Step("csl46", "assert", expect: "unit_asset", value: "human|wolf"),
|
||||||
Step("csl47", "assert", expect: "dossier_matches_focus"),
|
Step("csl47", "assert", expect: "dossier_matches_focus"),
|
||||||
|
// Theater lead hold across maintain ticks (no mob Follow thrash).
|
||||||
|
Step("csl47a", "remember_focus"),
|
||||||
|
Step("csl47b", "combat_maintain_focus"),
|
||||||
|
Step("csl47c", "wait", value: "0.35"),
|
||||||
|
Step("csl47d", "combat_maintain_focus"),
|
||||||
|
Step("csl47e", "wait", value: "0.35"),
|
||||||
|
Step("csl47f", "combat_maintain_focus"),
|
||||||
|
Step("csl47g", "assert", expect: "focus_same"),
|
||||||
|
|
||||||
// Chore bystander: park Follow on a sleeping rostered unit; maintain must retarget.
|
// Chore bystander: park Follow on a sleeping rostered unit; maintain must retarget.
|
||||||
Step("csl48", "combat_park_bystander_follow", asset: "human", value: "wolf"),
|
Step("csl48", "combat_park_bystander_follow", asset: "human", value: "wolf"),
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,13 @@ public sealed class InterestCandidate
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public long PairOwnerId;
|
public long PairOwnerId;
|
||||||
public long PairPartnerId;
|
public long PairPartnerId;
|
||||||
|
/// <summary>
|
||||||
|
/// Collective Mass/Battle camera owner: best fighter across both sticky sides.
|
||||||
|
/// Held across attack-target gaps so maintain ticks cannot thrash the follow.
|
||||||
|
/// </summary>
|
||||||
|
public long TheaterLeadId;
|
||||||
|
/// <summary>Unscaled time the theater lead was last a live combat participant.</summary>
|
||||||
|
public float TheaterLeadLastCombatAt = -999f;
|
||||||
public string Label = "";
|
public string Label = "";
|
||||||
public string AssetId = "";
|
public string AssetId = "";
|
||||||
public string SpeciesId = "";
|
public string SpeciesId = "";
|
||||||
|
|
@ -163,7 +170,11 @@ public sealed class InterestCandidate
|
||||||
|
|
||||||
public bool HasStickyCombatSides => Sticky.HasOpposingSides;
|
public bool HasStickyCombatSides => Sticky.HasOpposingSides;
|
||||||
|
|
||||||
public void ClearCombatSticky() => Sticky.Clear();
|
public void ClearCombatSticky()
|
||||||
|
{
|
||||||
|
Sticky.Clear();
|
||||||
|
ClearTheaterLead();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Lock durable 1v1 owner/partner ids (no-op when owner missing).</summary>
|
/// <summary>Lock durable 1v1 owner/partner ids (no-op when owner missing).</summary>
|
||||||
public void StampCombatPair(Actor owner, Actor partner)
|
public void StampCombatPair(Actor owner, Actor partner)
|
||||||
|
|
@ -188,6 +199,23 @@ public sealed class InterestCandidate
|
||||||
|
|
||||||
public bool HasCombatPairLock => PairOwnerId != 0;
|
public bool HasCombatPairLock => PairOwnerId != 0;
|
||||||
|
|
||||||
|
public void StampTheaterLead(Actor lead)
|
||||||
|
{
|
||||||
|
long id = EventFeedUtil.SafeId(lead);
|
||||||
|
if (id == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TheaterLeadId = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearTheaterLead()
|
||||||
|
{
|
||||||
|
TheaterLeadId = 0;
|
||||||
|
TheaterLeadLastCombatAt = -999f;
|
||||||
|
}
|
||||||
|
|
||||||
public InterestEvent ToInterestEvent()
|
public InterestEvent ToInterestEvent()
|
||||||
{
|
{
|
||||||
return new InterestEvent
|
return new InterestEvent
|
||||||
|
|
@ -227,6 +255,8 @@ public sealed class InterestCandidate
|
||||||
RelatedId = RelatedId,
|
RelatedId = RelatedId,
|
||||||
PairOwnerId = PairOwnerId,
|
PairOwnerId = PairOwnerId,
|
||||||
PairPartnerId = PairPartnerId,
|
PairPartnerId = PairPartnerId,
|
||||||
|
TheaterLeadId = TheaterLeadId,
|
||||||
|
TheaterLeadLastCombatAt = TheaterLeadLastCombatAt,
|
||||||
Label = Label,
|
Label = Label,
|
||||||
AssetId = AssetId,
|
AssetId = AssetId,
|
||||||
SpeciesId = SpeciesId,
|
SpeciesId = SpeciesId,
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,15 @@ public static class InterestDirector
|
||||||
private const int CombatFocusLargeParticipantThreshold = 16;
|
private const int CombatFocusLargeParticipantThreshold = 16;
|
||||||
/// <summary>Require this much extra focus weight before stealing the camera mid-fight.</summary>
|
/// <summary>Require this much extra focus weight before stealing the camera mid-fight.</summary>
|
||||||
private const float CombatFocusSwitchMargin = 12f;
|
private const float CombatFocusSwitchMargin = 12f;
|
||||||
|
/// <summary>
|
||||||
|
/// Collective Mass/Battle: hold theater lead through brief attack_target gaps
|
||||||
|
/// so maintain ticks cannot hop across equally-scored mob fighters.
|
||||||
|
/// </summary>
|
||||||
|
private const float CombatTheaterLeadGraceSeconds = 4f;
|
||||||
|
/// <summary>Only a clearly hotter fighter may steal theater lead mid-hold.</summary>
|
||||||
|
private const float CombatTheaterLeadSwitchMargin = 40f;
|
||||||
|
/// <summary>Boost the thinner sticky side's best (1-vs-mob spectacles).</summary>
|
||||||
|
private const float CombatTheaterLeadOutnumberedBonus = 28f;
|
||||||
private static readonly List<InterestCandidate> PendingScratch = new List<InterestCandidate>(96);
|
private static readonly List<InterestCandidate> PendingScratch = new List<InterestCandidate>(96);
|
||||||
|
|
||||||
public static string CurrentTierName => CurrentScoreLabel;
|
public static string CurrentTierName => CurrentScoreLabel;
|
||||||
|
|
@ -1021,70 +1030,79 @@ public static class InterestDirector
|
||||||
_current.PairPartnerId = 0;
|
_current.PairPartnerId = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prefer live fighters over rostered bystanders (Idle / Following / Breeding / chores).
|
// Collective Mass/Battle: hold the theater lead (best across both sides).
|
||||||
bool curFighting = LiveEnsemble.IsCombatParticipant(curFollow);
|
// Attack-target gaps used to hop the camera across the mob every maintain tick.
|
||||||
bool bestFighting = LiveEnsemble.IsCombatParticipant(best);
|
if (TryApplyTheaterLeadHold(_current, ref best, ref foe, fighters))
|
||||||
if (!curFighting)
|
|
||||||
{
|
{
|
||||||
if (bestFighting)
|
// Theater lead owns Follow/Related for this maintain.
|
||||||
{
|
|
||||||
foe = ResolveAttackFoe(best) ?? foe;
|
|
||||||
}
|
|
||||||
else if (TryRetargetCombatFollow(_current, out Actor fighter, out Actor fighterFoe))
|
|
||||||
{
|
|
||||||
best = fighter;
|
|
||||||
foe = fighterFoe ?? foe;
|
|
||||||
if (ensemble != null)
|
|
||||||
{
|
|
||||||
label = EventReason.Combat(ensemble);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (curFollow != null && curFollow.isAlive())
|
|
||||||
{
|
|
||||||
// Nobody fighting - keep ownership rather than jump to a random sleeper.
|
|
||||||
best = curFollow;
|
|
||||||
foe = ResolveAttackFoe(best) ?? curRelated ?? foe;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (!bestFighting)
|
else
|
||||||
{
|
{
|
||||||
best = curFollow;
|
// Prefer live fighters over rostered bystanders (Idle / Following / Breeding / chores).
|
||||||
foe = ResolveAttackFoe(best) ?? foe;
|
bool curFighting = LiveEnsemble.IsCombatParticipant(curFollow);
|
||||||
}
|
bool bestFighting = LiveEnsemble.IsCombatParticipant(best);
|
||||||
else if (best != curFollow
|
if (!curFighting)
|
||||||
&& curFollow != null
|
|
||||||
&& curFollow.isAlive()
|
|
||||||
&& !ShouldHoldDuelOwnership(_current, best, foe, fighters, curFollow, curRelated))
|
|
||||||
{
|
|
||||||
float curW = LiveEnsemble.FocusWeight(
|
|
||||||
curFollow, _current.ParticipantIds, newcomerBonus: 0f);
|
|
||||||
float newW = LiveEnsemble.FocusWeight(best, _current.ParticipantIds, newcomerBonus: 8f);
|
|
||||||
if (newW < curW + CombatFocusSwitchMargin)
|
|
||||||
{
|
{
|
||||||
best = curFollow;
|
if (bestFighting)
|
||||||
if (foe == null || foe == best)
|
|
||||||
{
|
{
|
||||||
foe = ResolveAttackFoe(best) ?? foe;
|
foe = ResolveAttackFoe(best) ?? foe;
|
||||||
}
|
}
|
||||||
|
else if (TryRetargetCombatFollow(_current, out Actor fighter, out Actor fighterFoe))
|
||||||
|
{
|
||||||
|
best = fighter;
|
||||||
|
foe = fighterFoe ?? foe;
|
||||||
|
if (ensemble != null)
|
||||||
|
{
|
||||||
|
label = EventReason.Combat(ensemble);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (curFollow != null && curFollow.isAlive())
|
||||||
|
{
|
||||||
|
// Nobody fighting - keep ownership rather than jump to a random sleeper.
|
||||||
|
best = curFollow;
|
||||||
|
foe = ResolveAttackFoe(best) ?? curRelated ?? foe;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!bestFighting)
|
||||||
|
{
|
||||||
|
best = curFollow;
|
||||||
|
foe = ResolveAttackFoe(best) ?? foe;
|
||||||
|
}
|
||||||
|
else if (best != curFollow
|
||||||
|
&& curFollow != null
|
||||||
|
&& curFollow.isAlive()
|
||||||
|
&& !ShouldHoldDuelOwnership(_current, best, foe, fighters, curFollow, curRelated))
|
||||||
|
{
|
||||||
|
float curW = LiveEnsemble.FocusWeight(
|
||||||
|
curFollow, _current.ParticipantIds, newcomerBonus: 0f);
|
||||||
|
float newW = LiveEnsemble.FocusWeight(best, _current.ParticipantIds, newcomerBonus: 8f);
|
||||||
|
if (newW < curW + CombatFocusSwitchMargin)
|
||||||
|
{
|
||||||
|
best = curFollow;
|
||||||
|
if (foe == null || foe == best)
|
||||||
|
{
|
||||||
|
foe = ResolveAttackFoe(best) ?? foe;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Preserve duel partner when rebuild briefly loses Related (attack_target gaps).
|
// Preserve duel partner when rebuild briefly loses Related (attack_target gaps).
|
||||||
bool duelLabeled = !string.IsNullOrEmpty(_current.Label)
|
bool duelLabeled = !string.IsNullOrEmpty(_current.Label)
|
||||||
&& _current.Label.StartsWith("Duel", StringComparison.OrdinalIgnoreCase);
|
&& _current.Label.StartsWith("Duel", StringComparison.OrdinalIgnoreCase);
|
||||||
if (foe == null
|
if (foe == null
|
||||||
&& curRelated != null
|
&& curRelated != null
|
||||||
&& curRelated.isAlive()
|
&& curRelated.isAlive()
|
||||||
&& curRelated != best
|
&& curRelated != best
|
||||||
&& (duelLabeled || _current.ForceActive || fighters <= 2))
|
&& (duelLabeled || _current.ForceActive || fighters <= 2))
|
||||||
{
|
{
|
||||||
foe = curRelated;
|
foe = curRelated;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Final pair ownership lock: Duel tips must not flip A↔B across maintain ticks.
|
// Final pair ownership lock: Duel tips must not flip A↔B across maintain ticks.
|
||||||
if (ShouldHoldDuelOwnership(_current, best, foe, fighters, curFollow, curRelated))
|
if (ShouldHoldDuelOwnership(_current, best, foe, fighters, curFollow, curRelated))
|
||||||
{
|
{
|
||||||
ApplyNamedPairHold(_current, curFollow, curRelated, ref best, ref foe, ref fighters, ref label);
|
ApplyNamedPairHold(_current, curFollow, curRelated, ref best, ref foe, ref fighters, ref label);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1789,6 +1807,272 @@ public static class InterestDirector
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Collective sticky Mass/Battle/Skirmish (not NamedPair duel ownership).
|
||||||
|
/// </summary>
|
||||||
|
private static bool IsCollectiveCombatTheater(InterestCandidate scene)
|
||||||
|
{
|
||||||
|
if (scene == null || scene.Completion != InterestCompletionKind.CombatActive)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsNamedPairCombatOwnership(scene))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!HasStickyCombatSides(scene))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return HasStickyCollectiveMulti(scene)
|
||||||
|
|| scene.CombatPeakParticipants >= 3
|
||||||
|
|| scene.ParticipantCount >= 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Score a sticky-side champion for theater-lead selection (stable, no newcomer jitter).
|
||||||
|
/// </summary>
|
||||||
|
private static float TheaterLeadScore(InterestCandidate scene, Actor actor, int ownSideCount, int foeSideCount)
|
||||||
|
{
|
||||||
|
if (actor == null || !actor.isAlive())
|
||||||
|
{
|
||||||
|
return -1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
float weight = LiveEnsemble.FocusWeight(actor, scene?.ParticipantIds, newcomerBonus: 0f);
|
||||||
|
if (weight < 0f)
|
||||||
|
{
|
||||||
|
return weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1-vs-mob: prefer the thinner side's champion (evil mage vs humans).
|
||||||
|
if (ownSideCount > 0
|
||||||
|
&& foeSideCount > 0
|
||||||
|
&& ownSideCount < foeSideCount
|
||||||
|
&& foeSideCount >= 3)
|
||||||
|
{
|
||||||
|
weight += CombatTheaterLeadOutnumberedBonus;
|
||||||
|
}
|
||||||
|
|
||||||
|
return weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pick the highest-scoring living fighter across both sticky sides.
|
||||||
|
/// </summary>
|
||||||
|
private static bool TryPickTheaterLead(
|
||||||
|
InterestCandidate scene,
|
||||||
|
out Actor lead,
|
||||||
|
out Actor foe)
|
||||||
|
{
|
||||||
|
lead = null;
|
||||||
|
foe = null;
|
||||||
|
if (scene?.Sticky == null || !scene.Sticky.HasOpposingSides)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LiveSceneStickyState sticky = scene.Sticky;
|
||||||
|
Actor pickA = LiveEnsemble.FindBestAliveTracked(sticky.SideAIds, preferCombat: true);
|
||||||
|
Actor pickB = LiveEnsemble.FindBestAliveTracked(sticky.SideBIds, preferCombat: true);
|
||||||
|
if ((pickA == null || !pickA.isAlive()) && (pickB == null || !pickB.isAlive()))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int countA = Mathf.Max(1, sticky.SideACount);
|
||||||
|
int countB = Mathf.Max(1, sticky.SideBCount);
|
||||||
|
float wA = TheaterLeadScore(scene, pickA, countA, countB);
|
||||||
|
float wB = TheaterLeadScore(scene, pickB, countB, countA);
|
||||||
|
if (wB > wA)
|
||||||
|
{
|
||||||
|
lead = pickB;
|
||||||
|
foe = pickA;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
lead = pickA;
|
||||||
|
foe = pickB;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lead != null && lead.isAlive();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Hold the collective theater lead through attack gaps; retarget only on death,
|
||||||
|
/// leaving the scrap past grace, or a clearly hotter fighter.
|
||||||
|
/// </summary>
|
||||||
|
private static bool TryApplyTheaterLeadHold(
|
||||||
|
InterestCandidate scene,
|
||||||
|
ref Actor best,
|
||||||
|
ref Actor foe,
|
||||||
|
int fighters)
|
||||||
|
{
|
||||||
|
if (scene == null || !IsCollectiveCombatTheater(scene))
|
||||||
|
{
|
||||||
|
if (fighters < 3 || scene == null || !HasStickyCombatSides(scene))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsNamedPairCombatOwnership(scene))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float now = Time.unscaledTime;
|
||||||
|
Actor held = null;
|
||||||
|
if (scene.TheaterLeadId != 0)
|
||||||
|
{
|
||||||
|
held = LiveEnsemble.FindTrackedActor(scene.TheaterLeadId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (held == null || !held.isAlive())
|
||||||
|
{
|
||||||
|
held = scene.FollowUnit != null && scene.FollowUnit.isAlive()
|
||||||
|
? scene.FollowUnit
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (held != null && LiveEnsemble.IsCombatParticipant(held))
|
||||||
|
{
|
||||||
|
scene.TheaterLeadLastCombatAt = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool onRoster = held != null && StickyScoreboard.IsOnRoster(scene, held);
|
||||||
|
bool inGrace = held != null
|
||||||
|
&& held.isAlive()
|
||||||
|
&& (LiveEnsemble.IsCombatParticipant(held)
|
||||||
|
|| now - scene.TheaterLeadLastCombatAt <= CombatTheaterLeadGraceSeconds);
|
||||||
|
bool holdOk = held != null && held.isAlive() && onRoster && inGrace;
|
||||||
|
|
||||||
|
if (!TryPickTheaterLead(scene, out Actor pick, out Actor pickFoe))
|
||||||
|
{
|
||||||
|
if (!holdOk)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
best = held;
|
||||||
|
foe = ResolveAttackFoe(held) ?? scene.RelatedUnit ?? foe;
|
||||||
|
scene.StampTheaterLead(held);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (holdOk)
|
||||||
|
{
|
||||||
|
float heldW = TheaterLeadScore(
|
||||||
|
scene,
|
||||||
|
held,
|
||||||
|
SideCountForActor(scene, held),
|
||||||
|
SideCountForOpponent(scene, held));
|
||||||
|
float pickW = TheaterLeadScore(
|
||||||
|
scene,
|
||||||
|
pick,
|
||||||
|
SideCountForActor(scene, pick),
|
||||||
|
SideCountForOpponent(scene, pick));
|
||||||
|
bool steal = pick != held
|
||||||
|
&& LiveEnsemble.IsCombatParticipant(pick)
|
||||||
|
&& pickW >= heldW + CombatTheaterLeadSwitchMargin;
|
||||||
|
if (!steal)
|
||||||
|
{
|
||||||
|
best = held;
|
||||||
|
foe = ResolveAttackFoe(held)
|
||||||
|
?? OppositeSideBest(scene, held)
|
||||||
|
?? pickFoe
|
||||||
|
?? foe;
|
||||||
|
scene.StampTheaterLead(held);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
best = pick;
|
||||||
|
foe = ResolveAttackFoe(pick) ?? pickFoe ?? foe;
|
||||||
|
scene.StampTheaterLead(pick);
|
||||||
|
if (LiveEnsemble.IsCombatParticipant(pick))
|
||||||
|
{
|
||||||
|
scene.TheaterLeadLastCombatAt = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SideCountForActor(InterestCandidate scene, Actor actor)
|
||||||
|
{
|
||||||
|
if (scene?.Sticky == null || actor == null)
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
long id = EventFeedUtil.SafeId(actor);
|
||||||
|
LiveSceneStickyState sticky = scene.Sticky;
|
||||||
|
for (int i = 0; i < sticky.SideAIds.Count; i++)
|
||||||
|
{
|
||||||
|
if (sticky.SideAIds[i] == id)
|
||||||
|
{
|
||||||
|
return Mathf.Max(1, sticky.SideACount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < sticky.SideBIds.Count; i++)
|
||||||
|
{
|
||||||
|
if (sticky.SideBIds[i] == id)
|
||||||
|
{
|
||||||
|
return Mathf.Max(1, sticky.SideBCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Mathf.Max(1, sticky.SideACount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SideCountForOpponent(InterestCandidate scene, Actor actor)
|
||||||
|
{
|
||||||
|
if (scene?.Sticky == null || actor == null)
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
long id = EventFeedUtil.SafeId(actor);
|
||||||
|
LiveSceneStickyState sticky = scene.Sticky;
|
||||||
|
for (int i = 0; i < sticky.SideAIds.Count; i++)
|
||||||
|
{
|
||||||
|
if (sticky.SideAIds[i] == id)
|
||||||
|
{
|
||||||
|
return Mathf.Max(1, sticky.SideBCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Mathf.Max(1, sticky.SideACount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Actor OppositeSideBest(InterestCandidate scene, Actor actor)
|
||||||
|
{
|
||||||
|
if (scene?.Sticky == null || actor == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
long id = EventFeedUtil.SafeId(actor);
|
||||||
|
LiveSceneStickyState sticky = scene.Sticky;
|
||||||
|
bool onA = false;
|
||||||
|
for (int i = 0; i < sticky.SideAIds.Count; i++)
|
||||||
|
{
|
||||||
|
if (sticky.SideAIds[i] == id)
|
||||||
|
{
|
||||||
|
onA = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return LiveEnsemble.FindBestAliveTracked(
|
||||||
|
onA ? sticky.SideBIds : sticky.SideAIds,
|
||||||
|
preferCombat: true);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Pick a living sticky-roster fighter when the current follow left the scrap.
|
/// Pick a living sticky-roster fighter when the current follow left the scrap.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -2253,16 +2537,37 @@ public static class InterestDirector
|
||||||
{
|
{
|
||||||
_current.PairOwnerId = 0;
|
_current.PairOwnerId = 0;
|
||||||
_current.PairPartnerId = 0;
|
_current.PairPartnerId = 0;
|
||||||
|
_current.ClearTheaterLead();
|
||||||
}
|
}
|
||||||
|
|
||||||
_current.FollowUnit = ensemble.Focus;
|
Actor focus = ensemble.Focus;
|
||||||
_current.SubjectId = EventFeedUtil.SafeId(ensemble.Focus);
|
Actor related = ensemble.Related;
|
||||||
_current.RelatedUnit = ensemble.Related;
|
if (IsCollectiveCombatTheater(_current)
|
||||||
_current.RelatedId = ensemble.Related != null ? EventFeedUtil.SafeId(ensemble.Related) : 0;
|
|| (authoredN >= 3 && HasStickyCombatSides(_current)))
|
||||||
|
{
|
||||||
|
if (TryPickTheaterLead(_current, out Actor lead, out Actor leadFoe))
|
||||||
|
{
|
||||||
|
focus = lead;
|
||||||
|
related = ResolveAttackFoe(lead) ?? leadFoe ?? related;
|
||||||
|
_current.StampTheaterLead(lead);
|
||||||
|
if (LiveEnsemble.IsCombatParticipant(lead))
|
||||||
|
{
|
||||||
|
_current.TheaterLeadLastCombatAt = Time.unscaledTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
ensemble.Focus = focus;
|
||||||
|
ensemble.Related = related;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_current.FollowUnit = focus;
|
||||||
|
_current.SubjectId = EventFeedUtil.SafeId(focus);
|
||||||
|
_current.RelatedUnit = related;
|
||||||
|
_current.RelatedId = related != null ? EventFeedUtil.SafeId(related) : 0;
|
||||||
_current.Label = EventReason.Combat(ensemble);
|
_current.Label = EventReason.Combat(ensemble);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_current.Position = ensemble.Focus.current_position;
|
_current.Position = focus.current_position;
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -597,6 +597,15 @@ public static class WorldActivityScanner
|
||||||
weight += 30f;
|
weight += 30f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Spectacle creatures (evil mage, dragon, …) win theater-lead picks in mob fights.
|
||||||
|
// Use a dedicated floor so char-only weights among civs cannot outrank them.
|
||||||
|
if (IsSpectacle(actor))
|
||||||
|
{
|
||||||
|
ScoringWeights w = InterestScoringConfig.W;
|
||||||
|
float spectacle = w.scannerSpectacleBonus > 0f ? w.scannerSpectacleBonus : 12f;
|
||||||
|
weight += Mathf.Max(35f, spectacle);
|
||||||
|
}
|
||||||
|
|
||||||
return weight;
|
return weight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "IdleSpectator",
|
"name": "IdleSpectator",
|
||||||
"author": "dazed",
|
"author": "dazed",
|
||||||
"version": "0.28.17",
|
"version": "0.28.18",
|
||||||
"description": "AFK Idle Spectator (I) + Lore (L). Killer button retargets dossier to the killer.",
|
"description": "AFK Idle Spectator (I) + Lore (L). Killer button retargets dossier to the killer.",
|
||||||
"GUID": "com.dazed.idlespectator"
|
"GUID": "com.dazed.idlespectator"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue