using System; using UnityEngine; namespace ProjectI.Enemies { public enum EnemyMovementContextKind { LocalEngagement, EscapingPursuit, Investigation, AlertOrientation, AllyReposition } // The movement owner's action lease issues the identity; copied geometry cannot issue authority. public sealed class EnemyMovementContext { private readonly EnemyActorContext owner; private readonly EnemyActionToken action; private readonly EnemyPressureDirector director; private readonly EnemyEngagementCycle cycle; private readonly ulong cycleGeneration; private readonly EnemyInvestigationEpisode investigation; private readonly EnemyActorContext reportSource; private readonly double reportUntil; private readonly double capturedAt; private readonly Vector3[] approvedRoute; private readonly Vector3 approvedOrigin, approvedGoal; private readonly float approvedLength; private readonly EnemyMoveLimits approvedLimits; private readonly double approvedUntil; public EnemyMovementContextKind Kind { get; } public ulong Id => action.Sequence; public ulong ActorSpawnIdentity => owner.SpawnIdentity; public ulong RunId => owner.RunId; public ulong StageId => owner.StageId; public ulong PursuitEpisodeId { get; } public ulong AssignmentId { get; } public EnemyPressureAssignmentPurpose AssignmentPurpose { get; } public double CapturedAt => capturedAt; public uint Participants { get; } internal Vector3? AssignmentArea { get; } public EnemyObservation? Evidence { get; } public double ExpiresAt { get; } internal Vector3 ApprovedGoal => approvedGoal; internal bool CycleBound => cycle != null; internal float ApprovedRouteLength => approvedLength; internal Vector3[] CopyApprovedRoute() => approvedRoute == null ? null : (Vector3[])approvedRoute.Clone(); internal bool IsCurrent(double now) => Current(owner, action, now); internal void EndPurpose() => action.Invalidate(); internal EnemyMovementContext(EnemyActorContext owner, EnemyActionToken action, EnemyMovementContextKind kind, EnemyEngagementCycle cycle, EnemyObservation? evidence, double expiresAt, EnemyInvestigationEpisode investigation = null, ulong assignmentId = 0) { if (owner == null || cycle == null || !action.IsValidFor(owner) || !EnemyObservationRules.Finite(expiresAt) || expiresAt <= owner.Now || kind == EnemyMovementContextKind.AlertOrientation || !evidence.HasValue && assignmentId == 0) throw new ArgumentException("Movement requires captured cycle, purpose, evidence or exact assignment."); this.owner = owner; this.action = action; this.cycle = cycle; this.investigation = investigation; director = owner.Encounter.PressureDirector; capturedAt = owner.Now; cycleGeneration = cycle.Generation; Kind = kind; Evidence = evidence; AssignmentId = assignmentId; PursuitEpisodeId = cycleGeneration; ExpiresAt = cycle.Phase == EnemyEngagementPhase.Pursue ? Math.Min(expiresAt, cycle.AttemptDeadline) : expiresAt; if (evidence?.Kind == EnemyObservationKind.SquadVisualReport) owner.Owner.GetComponent()?.TryCaptureReportLeaseServer(evidence.Value, capturedAt, out reportSource, out reportUntil); if (director == null || !director.CycleContextCurrentServer(owner, cycle, cycleGeneration, capturedAt)) throw new ArgumentException("Cycle admission has expired."); if (assignmentId != 0) { if (!director.TryCaptureMovementAssignmentServer(owner, assignmentId, out uint participants, out var area)) throw new ArgumentException("Movement requires exact live assignment participants."); Participants = participants; AssignmentArea = area; if (director.TryGetAssignmentServer(owner, out var assignment)) AssignmentPurpose = assignment.Purpose; } } internal EnemyMovementContext(EnemyActorContext owner, EnemyActionToken action, EnemyMovementContextKind kind, EnemyObservation? evidence, double expiresAt, EnemyInvestigationEpisode investigation = null) { if (owner == null || !action.IsValidFor(owner) || !EnemyObservationRules.Finite(expiresAt) || expiresAt <= owner.Now || kind != EnemyMovementContextKind.AlertOrientation && kind != EnemyMovementContextKind.Investigation || kind == EnemyMovementContextKind.AlertOrientation && (!evidence.HasValue || investigation != null) || kind == EnemyMovementContextKind.Investigation && owner.Encounter.PressureDirector != null) throw new ArgumentException("Observation needs its owned lease, evidence and original bounded purpose."); if (kind == EnemyMovementContextKind.AlertOrientation && (owner.Owner.GetComponent() == null || !owner.Owner.GetComponent().TryCaptureAlertServer(evidence.Value.Target, owner.Now, out var cue, out double until) || !cue.Equals(evidence.Value) || expiresAt > until)) throw new ArgumentException("Alert needs the exact captured cue and original expiry."); this.owner = owner; this.action = action; this.investigation = investigation; director = owner.Encounter.PressureDirector; capturedAt = owner.Now; Kind = kind; Evidence = evidence; ExpiresAt = expiresAt; if (kind != EnemyMovementContextKind.AlertOrientation && evidence?.Kind == EnemyObservationKind.SquadVisualReport) owner.Owner.GetComponent()?.TryCaptureReportLeaseServer(evidence.Value, capturedAt, out reportSource, out reportUntil); } private EnemyMovementContext(EnemyMovementContext source, EnemyActionToken action, EnemyMovePlan route = null) { owner = source.owner; this.action = action; director = source.director; cycle = source.cycle; cycleGeneration = source.cycleGeneration; investigation = source.investigation; capturedAt = source.capturedAt; Kind = source.Kind; reportSource = source.reportSource; reportUntil = source.reportUntil; Evidence = source.Evidence; ExpiresAt = source.ExpiresAt; PursuitEpisodeId = source.PursuitEpisodeId; AssignmentId = source.AssignmentId; Participants = source.Participants; AssignmentPurpose = source.AssignmentPurpose; AssignmentArea = source.AssignmentArea; approvedRoute = route != null ? route.CopyCorners() : source.approvedRoute; approvedOrigin = route?.Origin ?? source.approvedOrigin; approvedGoal = route?.ApproachGoal ?? source.approvedGoal; approvedLength = route?.ApproachRouteLength ?? source.approvedLength; approvedLimits = route?.Limits ?? source.approvedLimits; approvedUntil = route?.ValidUntil ?? source.approvedUntil; } internal EnemyMovementContext BindRoute(EnemyMovePlan fullRoute) => new EnemyMovementContext(this, action, fullRoute); internal EnemyMovementContext ContinueObservation(EnemyActionToken next) => new EnemyMovementContext(this, next); internal bool Authorizes(EnemyMovePlan plan) { if (Kind == EnemyMovementContextKind.AlertOrientation || plan.IsTransit && plan.RouteLength < .1f) return false; if (Kind == EnemyMovementContextKind.AllyReposition && !owner.Owner.GetComponent().AllyRouteAllowedServer(plan)) return false; if (approvedRoute == null || approvedRoute.Length == 0 || plan.CornerCount == 0 || plan.CornerCount > approvedRoute.Length || plan.Origin != approvedOrigin || plan.ApproachGoal != approvedGoal || plan.ApproachRouteLength != approvedLength || plan.ValidUntil > approvedUntil || plan.Limits.Speed > approvedLimits.Speed || plan.Limits.TravelDistance > approvedLimits.TravelDistance || plan.Limits.Seconds > approvedLimits.Seconds) return false; int full = 0; for (int i = 0; i < plan.CornerCount - 1; i++) { while (full > 0 && full < approvedRoute.Length && plan.Corner(i) != approvedRoute[full] && Vector3.Distance(approvedRoute[full - 1], approvedRoute[full]) <= .0001f) full++; if (full >= approvedRoute.Length || plan.Corner(i) != approvedRoute[full]) return false; full++; } while (full > 0 && full < approvedRoute.Length - 1 && Vector3.Distance(approvedRoute[full - 1], approvedRoute[full]) <= .0001f) full++; var endpoint = plan.Corner(plan.CornerCount - 1); if (full >= approvedRoute.Length) return endpoint == approvedRoute[approvedRoute.Length - 1]; if (full == 0) return endpoint == approvedRoute[0]; Vector3 a = approvedRoute[full - 1], b = approvedRoute[full], segment = b - a; Vector2 horizontal = new Vector2(segment.x, segment.z), offset = new Vector2(endpoint.x - a.x, endpoint.z - a.z); float fraction = horizontal.sqrMagnitude > .000001f ? Vector2.Dot(offset, horizontal) / horizontal.sqrMagnitude : 0; if (fraction < -.001f || fraction > 1.001f) return false; Vector3 expected = Vector3.Lerp(a, b, Mathf.Clamp01(fraction)); return new Vector2(endpoint.x - expected.x, endpoint.z - expected.z).magnitude <= .1f && Mathf.Abs(endpoint.y - expected.y) <= UnityEngine.AI.NavMesh.GetSettingsByID(plan.AgentType).agentClimb + .15f; } internal bool OwnedBy(EnemyActorContext actor, EnemyActionToken token) => ReferenceEquals(owner, actor) && action.Equals(token) && Id != 0; internal bool OwnsInvestigation(EnemyInvestigationEpisode episode) => episode != null && ReferenceEquals(investigation, episode) && action.Equals(episode.Action); internal bool PursuitCurrent(double now) => cycle != null && now >= capturedAt && now < ExpiresAt && director != null && director == owner.Encounter.PressureDirector && director.CycleContextCurrentServer(owner, cycle, cycleGeneration, now) && (Participants == 0 || director.MovementParticipantsEligibleServer(owner, Participants)) && (AssignmentId == 0 || director.TryGetAssignmentServer(owner, out var assignment) && assignment.Id == AssignmentId); internal bool Current(EnemyActorContext actor, EnemyActionToken token, double now) { if (!OwnedBy(actor, token) || !action.IsValidFor(actor) || !EnemyObservationRules.Finite(now) || now < capturedAt || now >= ExpiresAt || director != actor.Encounter.PressureDirector) return false; if (Evidence.HasValue) { var evidence = Evidence.Value; var perception = actor.Owner.GetComponent(); if (perception == null || now < evidence.ObservedAt || !perception.TryGetSnapshot(evidence.Target, now, out var snapshot)) return false; if (Kind == EnemyMovementContextKind.AlertOrientation) return actor.Owner.GetComponent()?.AlertCanContinueServer(evidence, now) == true && perception.AlertEvidenceCurrentServer(evidence, now, out double alertUntil) && ExpiresAt <= alertUntil; if (Kind == EnemyMovementContextKind.AllyReposition) return cycle != null && PursuitCurrent(now) && snapshot.Personal.HasValue && snapshot.Personal.Value.ObservedAt >= evidence.ObservedAt && evidence.Kind == EnemyObservationKind.PersonalVisual && evidence.Source.Equals(actor.Owner.Identity) && evidence.SourceSpawnIdentity == actor.SpawnIdentity && ExpiresAt <= evidence.ObservedAt + snapshot.EvidenceLifetimeSeconds && actor.Owner.GetComponent()?.AllyAuthorityCurrentServer(action, evidence, ExpiresAt, now) == true; if (Kind == EnemyMovementContextKind.LocalEngagement && evidence.Kind == EnemyObservationKind.PersonalVisual && !snapshot.VisibleAtLastSample) return false; if (evidence.Kind == EnemyObservationKind.SquadVisualReport) { // Pending knowledge may change source; this action retains its original witness and expiry. if (!perception.ReportLeaseCurrentServer(evidence, reportSource, reportUntil, now) || ExpiresAt > reportUntil) return false; } } if (investigation != null && (investigation.Terminal || now >= investigation.Deadline || !(actor.Owner.GetComponent()?.OwnsInvestigation(investigation) ?? false))) return false; if (PursuitEpisodeId != 0) return PursuitCurrent(now); return Kind == EnemyMovementContextKind.Investigation && director == null; } } }