- Update CameraDirector to log only significant tip changes - Introduce new combat framing change detection in EventReason - Enhance InterestDirector to manage theater lead and combat focus - Add new steps in HarnessScenarios for combat wire attack scenarios - Increment version to 0.28.24 in mod.json
318 lines
9.8 KiB
C#
318 lines
9.8 KiB
C#
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
namespace IdleSpectator;
|
|
|
|
public enum InterestLeadKind
|
|
{
|
|
EventLed = 0,
|
|
CharacterLed = 1
|
|
}
|
|
|
|
public enum InterestCompletionKind
|
|
{
|
|
/// <summary>One-shot narrative dwell (WorldLog, harness tips).</summary>
|
|
FixedDwell = 0,
|
|
/// <summary>Combat / hunt / fire while participants stay live.</summary>
|
|
CombatActive = 1,
|
|
/// <summary>Owning AI task / beat still live.</summary>
|
|
ActivityActive = 2,
|
|
/// <summary>Grief / family emotion with survivor + optional crying.</summary>
|
|
HappinessGrief = 3,
|
|
/// <summary>Status phase still present on subject.</summary>
|
|
StatusPhase = 4,
|
|
/// <summary>Character vignette until max watch or cold activity.</summary>
|
|
CharacterVignette = 5,
|
|
/// <summary>Harness-forced session; active until released or max cap.</summary>
|
|
Manual = 6,
|
|
/// <summary>Sticky war front while the war / kingdoms remain resolvable.</summary>
|
|
WarFront = 7,
|
|
/// <summary>Sticky plot cell while the plot / plotters remain resolvable.</summary>
|
|
PlotActive = 8,
|
|
/// <summary>Sticky family pack while members / alpha remain resolvable.</summary>
|
|
FamilyPack = 9,
|
|
/// <summary>Sticky status outbreak while clustered carriers remain resolvable.</summary>
|
|
StatusOutbreak = 10,
|
|
/// <summary>Sticky while <c>Earthquake.isQuakeActive()</c> (ongoing shake poll).</summary>
|
|
EarthquakeActive = 11
|
|
}
|
|
|
|
/// <summary>
|
|
/// Durable interest scene candidate. <see cref="TotalScore"/> is the sole ranking signal;
|
|
/// typed fields (lead, completion, combat) gate interrupt policy.
|
|
/// Multi-actor sticky scoreboard lives on <see cref="Sticky"/>.
|
|
/// </summary>
|
|
public sealed class InterestCandidate
|
|
{
|
|
public string Key = "";
|
|
public InterestLeadKind LeadKind = InterestLeadKind.EventLed;
|
|
public string Category = "";
|
|
public string Source = "";
|
|
public float EventStrength;
|
|
public float CharacterSignificance;
|
|
public float Novelty = 1f;
|
|
public float VisualConfidence = 0.5f;
|
|
public float TotalScore;
|
|
/// <summary>Live fighters / participants in a cluster (battles, multi-unit events).</summary>
|
|
public int ParticipantCount;
|
|
/// <summary>Sticky multi-actor scoreboard (combat first; war/plot/family later).</summary>
|
|
public readonly LiveSceneStickyState Sticky = new LiveSceneStickyState();
|
|
/// <summary>Notable (king/favorite/leader/high renown) participants in the cluster.</summary>
|
|
public int NotableParticipantCount;
|
|
public Vector3 Position;
|
|
public Actor FollowUnit;
|
|
public Actor RelatedUnit;
|
|
public long SubjectId;
|
|
public long RelatedId;
|
|
/// <summary>
|
|
/// Durable 1v1 ownership: first Follow stays camera owner while both pair ids live.
|
|
/// Prevents Duel A↔B thrash across maintain ticks and registry upserts.
|
|
/// </summary>
|
|
public long PairOwnerId;
|
|
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 AssetId = "";
|
|
public string SpeciesId = "";
|
|
public string Verb = "";
|
|
public string CityKey = "";
|
|
public string KingdomKey = "";
|
|
public string RegionKey = "";
|
|
public string HappinessEffectId = "";
|
|
public string StatusId = "";
|
|
public string CorrelationKey = "";
|
|
public float CreatedAt;
|
|
public float LastSeenAt;
|
|
public float ExpiresAt;
|
|
public float MinWatch = 1.5f;
|
|
public float MaxWatch = 45f;
|
|
public InterestCompletionKind Completion = InterestCompletionKind.FixedDwell;
|
|
public bool Resumable = true;
|
|
public bool ForceActive;
|
|
public bool Selected;
|
|
public string ScoreDetail = "";
|
|
public readonly List<long> ParticipantIds = new List<long>(4);
|
|
|
|
// Compat forwarders - existing combat maintain/harness code.
|
|
public int CombatPeakParticipants
|
|
{
|
|
get => Sticky.PeakParticipants;
|
|
set => Sticky.PeakParticipants = value;
|
|
}
|
|
|
|
public EnsembleFrame CombatSideFrame
|
|
{
|
|
get => Sticky.Frame;
|
|
set => Sticky.Frame = value;
|
|
}
|
|
|
|
public string CombatSideAKey
|
|
{
|
|
get => Sticky.SideAKey;
|
|
set => Sticky.SideAKey = value;
|
|
}
|
|
|
|
public string CombatSideADisplay
|
|
{
|
|
get => Sticky.SideADisplay;
|
|
set => Sticky.SideADisplay = value;
|
|
}
|
|
|
|
public string CombatSideAKingdom
|
|
{
|
|
get => Sticky.SideAKingdom;
|
|
set => Sticky.SideAKingdom = value;
|
|
}
|
|
|
|
public int CombatSideACount
|
|
{
|
|
get => Sticky.SideACount;
|
|
set => Sticky.SideACount = value;
|
|
}
|
|
|
|
public string CombatSideBKey
|
|
{
|
|
get => Sticky.SideBKey;
|
|
set => Sticky.SideBKey = value;
|
|
}
|
|
|
|
public string CombatSideBDisplay
|
|
{
|
|
get => Sticky.SideBDisplay;
|
|
set => Sticky.SideBDisplay = value;
|
|
}
|
|
|
|
public string CombatSideBKingdom
|
|
{
|
|
get => Sticky.SideBKingdom;
|
|
set => Sticky.SideBKingdom = value;
|
|
}
|
|
|
|
public int CombatSideBCount
|
|
{
|
|
get => Sticky.SideBCount;
|
|
set => Sticky.SideBCount = value;
|
|
}
|
|
|
|
public List<long> CombatSideAIds => Sticky.SideAIds;
|
|
public List<long> CombatSideBIds => Sticky.SideBIds;
|
|
|
|
public bool HasFollowUnit => FollowUnit != null && FollowUnit.isAlive();
|
|
|
|
public bool HasValidPosition =>
|
|
HasFollowUnit || (Position != Vector3.zero && !float.IsNaN(Position.x));
|
|
|
|
public bool HasStickyCombatSides => Sticky.HasOpposingSides;
|
|
|
|
public void ClearCombatSticky()
|
|
{
|
|
// Keep TheaterLeadId across sticky rebuilds / tip reframes so Mass focus
|
|
// does not hop when camps briefly lose opposing-side identity.
|
|
Sticky.Clear();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Lock durable 1v1 owner/partner ids (no-op when owner missing).
|
|
/// First living partner sticks across attack_target swaps; replace only when the
|
|
/// locked partner is dead/missing (or <paramref name="replacePartner"/>).
|
|
/// </summary>
|
|
public void StampCombatPair(Actor owner, Actor partner, bool replacePartner = false)
|
|
{
|
|
long oid = EventFeedUtil.SafeId(owner);
|
|
if (oid == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (PairOwnerId == 0)
|
|
{
|
|
PairOwnerId = oid;
|
|
}
|
|
|
|
long pid = EventFeedUtil.SafeId(partner);
|
|
if (pid == 0 || pid == PairOwnerId)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (PairPartnerId == 0 || replacePartner)
|
|
{
|
|
PairPartnerId = pid;
|
|
return;
|
|
}
|
|
|
|
if (PairPartnerId == pid)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Locked partner still alive: keep them. Dead/missing: adopt the new foe once.
|
|
Actor locked = LiveEnsemble.FindTrackedActor(PairPartnerId);
|
|
if (locked == null || !locked.isAlive())
|
|
{
|
|
PairPartnerId = pid;
|
|
}
|
|
}
|
|
|
|
public bool HasCombatPairLock => PairOwnerId != 0;
|
|
|
|
public void StampTheaterLead(Actor lead)
|
|
{
|
|
long id = EventFeedUtil.SafeId(lead);
|
|
if (id == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// New lead must not inherit the previous unit's attack-gap grace.
|
|
if (TheaterLeadId != 0 && TheaterLeadId != id)
|
|
{
|
|
TheaterLeadLastCombatAt = -999f;
|
|
}
|
|
|
|
TheaterLeadId = id;
|
|
}
|
|
|
|
public void ClearTheaterLead()
|
|
{
|
|
TheaterLeadId = 0;
|
|
TheaterLeadLastCombatAt = -999f;
|
|
}
|
|
|
|
public InterestEvent ToInterestEvent()
|
|
{
|
|
return new InterestEvent
|
|
{
|
|
Score = TotalScore,
|
|
Position = Position,
|
|
FollowUnit = FollowUnit,
|
|
RelatedUnit = RelatedUnit,
|
|
Label = Label,
|
|
CreatedAt = CreatedAt,
|
|
AssetId = AssetId,
|
|
ParticipantCount = ParticipantCount,
|
|
NotableParticipantCount = NotableParticipantCount,
|
|
CharacterSignificance = CharacterSignificance
|
|
};
|
|
}
|
|
|
|
public InterestCandidate CloneShallow()
|
|
{
|
|
var copy = new InterestCandidate
|
|
{
|
|
Key = Key,
|
|
LeadKind = LeadKind,
|
|
Category = Category,
|
|
Source = Source,
|
|
EventStrength = EventStrength,
|
|
CharacterSignificance = CharacterSignificance,
|
|
Novelty = Novelty,
|
|
VisualConfidence = VisualConfidence,
|
|
TotalScore = TotalScore,
|
|
ParticipantCount = ParticipantCount,
|
|
NotableParticipantCount = NotableParticipantCount,
|
|
Position = Position,
|
|
FollowUnit = FollowUnit,
|
|
RelatedUnit = RelatedUnit,
|
|
SubjectId = SubjectId,
|
|
RelatedId = RelatedId,
|
|
PairOwnerId = PairOwnerId,
|
|
PairPartnerId = PairPartnerId,
|
|
TheaterLeadId = TheaterLeadId,
|
|
TheaterLeadLastCombatAt = TheaterLeadLastCombatAt,
|
|
Label = Label,
|
|
AssetId = AssetId,
|
|
SpeciesId = SpeciesId,
|
|
Verb = Verb,
|
|
CityKey = CityKey,
|
|
KingdomKey = KingdomKey,
|
|
RegionKey = RegionKey,
|
|
HappinessEffectId = HappinessEffectId,
|
|
StatusId = StatusId,
|
|
CorrelationKey = CorrelationKey,
|
|
CreatedAt = CreatedAt,
|
|
LastSeenAt = LastSeenAt,
|
|
ExpiresAt = ExpiresAt,
|
|
MinWatch = MinWatch,
|
|
MaxWatch = MaxWatch,
|
|
Completion = Completion,
|
|
Resumable = Resumable,
|
|
ForceActive = ForceActive,
|
|
Selected = Selected,
|
|
ScoreDetail = ScoreDetail
|
|
};
|
|
Sticky.CopyTo(copy.Sticky);
|
|
for (int i = 0; i < ParticipantIds.Count; i++)
|
|
{
|
|
copy.ParticipantIds.Add(ParticipantIds[i]);
|
|
}
|
|
|
|
return copy;
|
|
}
|
|
}
|