using System; using UnityEngine; namespace ProjectI.Enemies { public enum EnemyInvestigationPhase { Planning, Travelling, Inspecting, Exhausted, Expired, Reacquired } public enum EnemyInvestigationReason { None, NoUsefulViewpoint, NoEligibleGround, TimeLimit, EvidenceExpired, TravelLimit, AttemptLimit, InspectionLimit, Reacquired, InvalidTarget } // One immutable sighting identity and bounded history. This object never queries the scene or writes transforms. public sealed class EnemyInvestigationEpisode { public EnemyTargetKey Target { get; } public EnemyInvestigationSettings Settings { get; } public Vector3 Anchor { get; } public Vector3 StartingView { get; } public Vector3 ObservedFrom { get; } public Vector3 Axis { get; } public Vector3 Velocity { get; } public double SightAt { get; } public double Deadline { get; } public double EvidenceExpiresAt { get; } public double FirstAdmissionAt { get; private set; } = double.NaN; public EnemyInvestigationPhase Phase { get; internal set; } public EnemyInvestigationReason Reason { get; private set; } public float Travelled { get; private set; } public int Attempts { get; private set; } public int Inspections { get; private set; } public int ActiveCandidate { get; private set; } = -1; public EnemyActionToken Action { get; private set; } public Vector3 LookPoint { get; internal set; } public float ActiveGain { get; internal set; } public float ActiveScore { get; internal set; } public bool Terminal => Phase == EnemyInvestigationPhase.Exhausted || Phase == EnemyInvestigationPhase.Expired || Phase == EnemyInvestigationPhase.Reacquired; public float TravelRemaining => Mathf.Max(0, Settings.MaximumSearchTravelDistance - Travelled); internal readonly Hypothesis[] hypotheses; internal readonly Viewpoint[] viewpoints; internal int hypothesisCount, viewpointCount; internal int PendingCandidate = -1; internal int generationIndex, weightedCount; internal int coverIndex, coverGroundStep, coverGroundSteps, coverStage, coverCount, coverScanSteps; internal Vector3 coverRequested, coverSurface, coverRay, coverExitPoint, coverExitNormal, pendingHypothesis; internal float coverDistance, coverLimit, coverEntry, coverExit, coverFirstExit; internal Collider coverWall; internal bool coverComplete, hasPendingHypothesis, pendingCover; internal bool generationComplete, weightsComplete, candidatesComplete; #if UNITY_EDITOR // One latest executed forward-query result per cone direction, never an extra physics query. public enum CoverRayOutcome { NotCast, WallHit, OtherWorldHit, Miss, Saturated } public readonly struct CoverRaySample { public readonly Vector3 Origin, End; public readonly CoverRayOutcome Outcome; internal CoverRaySample(Vector3 origin, Vector3 end, CoverRayOutcome outcome) { Origin = origin; End = end; Outcome = outcome; } } private readonly CoverRaySample[] coverRays; public int CoverRayCapacity => coverRays.Length; public bool TryGetCoverRaySample(int index, out CoverRaySample sample) { sample = index >= 0 && index < coverRays.Length ? coverRays[index] : default; return sample.Outcome != CoverRayOutcome.NotCast; } internal void RecordCoverRay(Vector3 end, CoverRayOutcome outcome) { coverRays[coverIndex] = new CoverRaySample(ObservedFrom, end, outcome); } internal void ClearCoverRays() => Array.Clear(coverRays, 0, coverRays.Length); public bool HypothesisFromWallHit(int index) => hypotheses[index].FromWallHit; #endif internal uint inspectedMask; internal float initialWeight, accountedDistance; internal double facingSince = double.NaN, lastInspectionSample = double.NaN; internal uint actualCoverage; internal int inspectionProbe; internal EnemyPerceptionSnapshot inspectionSnapshot; internal bool hasInspectionSnapshot; internal bool awaitingObservation; internal int inspectionGeometry; internal sealed class Hypothesis { internal Vector3 Point; internal float Weight; #if UNITY_EDITOR internal bool FromWallHit; #endif } internal sealed class Viewpoint { internal Vector3 Point; internal bool Exhausted, Failed; internal double FailedAt; internal int FailedGeometry; internal uint Coverage; internal float Gain, Score, RouteLength; internal EnemyMovePlan Route; internal Vector3 LookPoint; internal bool RefinedOrientation; } private EnemyInvestigationEpisode(EnemyPerceptionSnapshot snapshot, EnemyInvestigationSettings settings, Vector3 start, Vector3 forward) { Target = snapshot.Target; Settings = settings; Anchor = snapshot.CurrentMotion.Value.Position; StartingView = start; ObservedFrom = snapshot.ObservedFrom; SightAt = snapshot.Personal.Value.ObservedAt; EvidenceExpiresAt = snapshot.PersonalExpiresAt; Deadline = Math.Min(SightAt + settings.MaximumSearchSeconds, EvidenceExpiresAt); Velocity = CapturedVelocity(snapshot, settings); Vector3 axis = Velocity.sqrMagnitude > .000001f ? Velocity : Anchor - ObservedFrom; axis.y = 0; if (axis.sqrMagnitude < .000001f) { axis = forward; axis.y = 0; } Axis = axis.sqrMagnitude > .000001f ? axis.normalized : Vector3.forward; hypotheses = new Hypothesis[settings.MaximumHypotheses]; viewpoints = new Viewpoint[settings.MaximumCandidates]; #if UNITY_EDITOR coverRays = new CoverRaySample[settings.CoverFanSampleCount]; #endif } public static bool TryBegin(EnemyPerceptionSnapshot snapshot, EnemyInvestigationSettings settings, Vector3 start, Vector3 forward, double now, out EnemyInvestigationEpisode episode) { episode = null; if (settings == null || snapshot.VisibleAtLastSample || !snapshot.Personal.HasValue || !snapshot.CurrentMotion.HasValue || snapshot.Personal.Value.Kind != EnemyObservationKind.PersonalVisual || !snapshot.Personal.Value.Target.Equals(snapshot.Target) || snapshot.Personal.Value.ObservedAt != snapshot.CurrentMotion.Value.ObservedAt || !EnemyObservationRules.Finite(start) || !EnemyObservationRules.Finite(forward) || !EnemyObservationRules.Finite(snapshot.ObservedFrom) || !EnemyObservationRules.Finite(snapshot.CurrentMotion.Value.Position) || !EnemyObservationRules.Finite(now) || now < snapshot.Personal.Value.ObservedAt || now >= snapshot.PersonalExpiresAt || now >= snapshot.Personal.Value.ObservedAt + settings.MaximumSearchSeconds) return false; episode = new EnemyInvestigationEpisode(snapshot, settings, start, forward); return true; } public static Vector3 CapturedVelocity(EnemyPerceptionSnapshot snapshot, EnemyInvestigationSettings settings) { if (!snapshot.CurrentMotion.HasValue || !snapshot.PreviousMotion.HasValue) return Vector3.zero; var a = snapshot.PreviousMotion.Value; var b = snapshot.CurrentMotion.Value; double seconds = b.ObservedAt - a.ObservedAt; Vector3 delta = b.Position - a.Position; delta.y = 0; if (!EnemyObservationRules.Finite(seconds) || !EnemyObservationRules.Finite(delta) || seconds <= 0 || seconds > settings.DirectionMaximumGap || delta.magnitude < settings.DirectionMinimumDisplacement || delta.magnitude / seconds > settings.MaximumInferredSpeed) return Vector3.zero; return delta / (float)seconds; } public bool Matches(EnemyPerceptionSnapshot snapshot) => Target.Equals(snapshot.Target) && snapshot.Personal.HasValue && SightAt == snapshot.Personal.Value.ObservedAt; public void Refresh(double now) { if (Terminal) return; if (!EnemyObservationRules.Finite(now) || now < SightAt) { Finish(EnemyInvestigationReason.InvalidTarget); return; } if (now >= Deadline) { Finish(now >= EvidenceExpiresAt ? EnemyInvestigationReason.EvidenceExpired : EnemyInvestigationReason.TimeLimit); return; } if (TravelRemaining <= .001f) Finish(EnemyInvestigationReason.TravelLimit); else if (Phase == EnemyInvestigationPhase.Planning && Inspections >= Settings.MaximumInspections) Finish(EnemyInvestigationReason.InspectionLimit); else if (Phase == EnemyInvestigationPhase.Planning && Attempts >= Settings.MaximumAttempts) Finish(EnemyInvestigationReason.AttemptLimit); } internal bool Admit(int candidate, EnemyActionToken token, bool observing, double now) { Refresh(now); if (Terminal || Phase != EnemyInvestigationPhase.Planning || candidate < 0 || candidate >= viewpointCount || token.Sequence == 0) return false; if (Attempts == 0) FirstAdmissionAt = now; Attempts++; ActiveCandidate = candidate; Action = token; accountedDistance = 0; Phase = observing ? EnemyInvestigationPhase.Inspecting : EnemyInvestigationPhase.Travelling; ResetInspection(); return true; } internal void ObserveAfterArrival(EnemyActionToken token) { Action = token; accountedDistance = 0; awaitingObservation = false; Phase = EnemyInvestigationPhase.Inspecting; ResetInspection(); } public void ChargeTravel(EnemyActionToken token, float cumulative) { if (!Action.Equals(token) || !EnemyObservationRules.Finite(cumulative) || cumulative < accountedDistance) return; Travelled += cumulative - accountedDistance; accountedDistance = cumulative; } internal void EndAction(bool completedInspection, uint checkedMask, double now, int geometry) { if (ActiveCandidate >= 0) { var view = viewpoints[ActiveCandidate]; if (completedInspection) { Inspections++; inspectedMask |= checkedMask; view.Exhausted = true; } else { view.Failed = true; view.FailedAt = now; view.FailedGeometry = geometry; } } Action = default; ActiveCandidate = -1; awaitingObservation = false; ResetInspection(); if (!Terminal) Phase = EnemyInvestigationPhase.Planning; Refresh(now); } internal void ResetInspection() { facingSince = double.NaN; lastInspectionSample = double.NaN; actualCoverage = 0; inspectionProbe = 0; hasInspectionSnapshot = false; } public void Finish(EnemyInvestigationReason reason) { if (Terminal) return; Reason = reason; Phase = reason == EnemyInvestigationReason.Reacquired ? EnemyInvestigationPhase.Reacquired : reason == EnemyInvestigationReason.TimeLimit || reason == EnemyInvestigationReason.EvidenceExpired ? EnemyInvestigationPhase.Expired : EnemyInvestigationPhase.Exhausted; } #if UNITY_EDITOR || DEVELOPMENT_BUILD public int HypothesisCount => hypothesisCount; public int ViewpointCount => viewpointCount; public Vector3 HypothesisPoint(int index) => hypotheses[index].Point; public float HypothesisWeight(int index) => initialWeight > 0 ? hypotheses[index].Weight / initialWeight : 0; public bool HypothesisChecked(int index) => (inspectedMask & (1u << index)) != 0; public Vector3 ViewpointPoint(int index) => viewpoints[index].Point; public float ViewpointGain(int index) => viewpoints[index].Gain; public float ViewpointScore(int index) => viewpoints[index].Score; public bool ViewpointFailed(int index) => viewpoints[index].Failed; public bool ViewpointCompleted(int index) => viewpoints[index].Exhausted; #endif } }