using System; namespace ProjectI.Enemies { public enum EnemyEngagementPhase { Available, Pursue, Fight, Breakoff } public sealed class EnemyEngagementCycle { private readonly double attemptSeconds, combatConfirmSeconds, breakoffSeconds; private double lastTime = -1, sampledAt = -1, coveredUntil, candidateStart; private bool closed, candidate, confirmed; public EnemyEngagementCycle(double attemptSeconds, double combatConfirmSeconds, double breakoffSeconds) { RequireDuration(attemptSeconds, nameof(attemptSeconds)); RequireDuration(combatConfirmSeconds, nameof(combatConfirmSeconds)); RequireDuration(breakoffSeconds, nameof(breakoffSeconds)); this.attemptSeconds = attemptSeconds; this.combatConfirmSeconds = combatConfirmSeconds; this.breakoffSeconds = breakoffSeconds; } public EnemyEngagementPhase Phase { get; private set; } public bool IsClosed => closed; public ulong Generation { get; private set; } public ulong CombatConfirmationSequence { get; private set; } public double CombatConfirmedAt { get; private set; } public double AttemptDeadline { get; private set; } public double BreakoffUntil { get; private set; } public bool TryBeginAttempt(double now) { if (!AcceptTime(now) || ExpireAttempt(now) || Phase != EnemyEngagementPhase.Available) return false; return BeginAttempt(now); } public void Observe(double now, bool usefulCombat, double validUntil) => ObserveCore(now, usefulCombat, validUntil, false); // The attack owner emits this once per newly admitted melee action, never for retries/ticks. public void ObserveMeleeAttack(double now, double validUntil) => ObserveCore(now, true, validUntil, true); private void ObserveCore(double now, bool usefulCombat, double validUntil, bool admittedMelee) { if (!AcceptTime(now) || ExpireAttempt(now)) return; bool continuous = candidate && now >= sampledAt && now - sampledAt <= 1 && now <= coveredUntil; bool useful = usefulCombat && Finite(validUntil) && validUntil > now; if (!continuous || !useful) { ResetConfirmation(); if (Phase == EnemyEngagementPhase.Fight && !BeginAttempt(now)) return; } if (useful) { if (!candidate) { candidate = true; candidateStart = now; } sampledAt = now; coveredUntil = Math.Min(validUntil, now + 1); if (!confirmed && (admittedMelee || now - candidateStart >= combatConfirmSeconds)) { confirmed = true; CombatConfirmationSequence++; CombatConfirmedAt = now; } } if (Phase == EnemyEngagementPhase.Breakoff) { if (now < BreakoffUntil) return; BreakoffUntil = 0; Transition(confirmed ? EnemyEngagementPhase.Fight : EnemyEngagementPhase.Available); } else if (confirmed && Phase != EnemyEngagementPhase.Fight) { AttemptDeadline = 0; Transition(EnemyEngagementPhase.Fight); } } public void Close() { if (closed) return; closed = true; ResetConfirmation(); AttemptDeadline = BreakoffUntil = 0; Transition(EnemyEngagementPhase.Available); } private bool AcceptTime(double now) { if (closed) return false; if (!Finite(now) || now < 0 || now < lastTime) { Close(); return false; } lastTime = now; return true; } private bool BeginAttempt(double now) { if (!TryDeadline(now, attemptSeconds, out double deadline)) return false; AttemptDeadline = deadline; Transition(EnemyEngagementPhase.Pursue); return true; } private bool ExpireAttempt(double now) { if (Phase != EnemyEngagementPhase.Pursue || now < AttemptDeadline) return false; ResetConfirmation(); AttemptDeadline = 0; // A stalled owner must still observe the full interruption; never catch up cycles. if (TryDeadline(now, breakoffSeconds, out double deadline)) { BreakoffUntil = deadline; Transition(EnemyEngagementPhase.Breakoff); } return true; } private bool TryDeadline(double now, double seconds, out double deadline) { deadline = now + seconds; if (Finite(deadline) && deadline > now) return true; Close(); return false; } private void ResetConfirmation() { candidate = confirmed = false; sampledAt = -1; coveredUntil = candidateStart = 0; } private void Transition(EnemyEngagementPhase phase) { Phase = phase; Generation++; } private static bool Finite(double value) => !double.IsNaN(value) && !double.IsInfinity(value); private static void RequireDuration(double value, string name) { if (!Finite(value) || value <= 0) throw new ArgumentOutOfRangeException(name); } } }