using System; using UnityEngine; namespace ProjectI.Enemies { // Per-target state, owned only by the containing enemy's perception capability. public sealed class EnemyObservationRules { public EnemyTargetKey Key { get; } public EnemyObservation? Personal { get; private set; } public EnemyObservation? Report { get; private set; } public EnemyObservation? Heard { get; private set; } public EnemyObservedMotionSample? PreviousMotion { get; private set; } public EnemyObservedMotionSample? CurrentMotion { get; private set; } public Vector3 ObservedFrom { get; private set; } private bool motionContinuous; public bool Visible { get; private set; } public bool ReactionReady { get; private set; } public double PersonalExpiresAt => Personal.HasValue ? Personal.Value.ObservedAt + memorySeconds : double.NaN; internal double MemorySeconds => memorySeconds; private readonly double reactionDelay, memorySeconds, maximumSampleGap; private double acquisitionAt = double.NaN; public EnemyObservationRules(EnemyTargetKey key, double reactionDelay = .25, double memorySeconds = 3, double maximumSampleGap = .3) { if (key.SpawnLifetime == 0 || !Finite(reactionDelay) || reactionDelay < 0 || !Finite(memorySeconds) || memorySeconds <= 0 || !Finite(maximumSampleGap) || maximumSampleGap <= 0) throw new ArgumentException("Invalid target memory tuning/identity."); Key = key; this.reactionDelay = reactionDelay; this.memorySeconds = memorySeconds; this.maximumSampleGap = maximumSampleGap; } public bool Observe(EnemyObservation observation, double now) { if (!Valid(observation, now) || !observation.Target.Equals(Key)) return false; Expire(now); switch (observation.Kind) { case EnemyObservationKind.PersonalVisual: if (observation.ObservedAt != now || Personal.HasValue && observation.ObservedAt <= Personal.Value.ObservedAt) return false; if (!ReactionReady && (!Visible || !Personal.HasValue || now - Personal.Value.ObservedAt > maximumSampleGap)) acquisitionAt = now; Personal = observation; Visible = true; ReactionReady |= now - acquisitionAt >= reactionDelay; return true; case EnemyObservationKind.SquadVisualReport: if (Report.HasValue && observation.ObservedAt <= Report.Value.ObservedAt) return false; Report = observation; return true; case EnemyObservationKind.HeardRegion: if (Heard.HasValue && observation.ObservedAt <= Heard.Value.ObservedAt) return false; Heard = observation; return true; default: return false; } } public void Miss(double now) { Expire(now); Visible = false; motionContinuous = false; if (!ReactionReady) acquisitionAt = double.NaN; } // Called only after successful real sight; aim selection can change height independently of feet. public bool ObservePersonal(EnemyObservation observation, Vector3 feet, Vector3 observer, double now) { if (observation.Kind != EnemyObservationKind.PersonalVisual || !Finite(feet) || !Finite(observer)) return false; Expire(now); bool contiguous = motionContinuous && Visible && CurrentMotion.HasValue && now > CurrentMotion.Value.ObservedAt; if (!Observe(observation, now)) return false; PreviousMotion = contiguous ? CurrentMotion : null; CurrentMotion = new EnemyObservedMotionSample(feet, now); ObservedFrom = observer; motionContinuous = true; return true; } private void Expire(double now) { if (!Personal.HasValue || !Fresh(Personal.Value, now)) { Visible = ReactionReady = motionContinuous = false; acquisitionAt = double.NaN; } } private bool Fresh(EnemyObservation observation, double now) => Finite(now) && now >= observation.ObservedAt && now - observation.ObservedAt < memorySeconds; public static bool Finite(double value) => !double.IsNaN(value) && !double.IsInfinity(value); public static bool Finite(Vector3 value) => Finite(value.x) && Finite(value.y) && Finite(value.z); private bool Valid(EnemyObservation observation, double now) => Finite(now) && now >= 0 && Finite(observation.ObservedAt) && observation.ObservedAt >= 0 && Fresh(observation, now) && Finite(observation.Position) && (observation.Kind == EnemyObservationKind.HeardRegion ? Finite(observation.RegionRadius) && observation.RegionRadius > 0 : observation.RegionRadius == 0 && observation.Source.Lifetime != 0 && observation.SourceSpawnIdentity != 0); public bool TryPersonal(double now, out EnemyObservation observation) { Expire(now); observation = Personal.GetValueOrDefault(); return Personal.HasValue && Fresh(observation, now); } public bool TryBest(double now, out EnemyObservation observation, out int quality) { if (TryPersonal(now, out observation)) { quality = Visible ? 3 : 2; return true; } if (Report.HasValue && Fresh(Report.Value, now)) { observation = Report.Value; quality = 1; return true; } if (Heard.HasValue && Fresh(Heard.Value, now)) { observation = Heard.Value; quality = 0; return true; } quality = -1; observation = default; return false; } } }