OMRBotController.CombatHumanization.cs
using Sandbox;
using System;

public enum OMRBotCombatSequenceKind
{
    None,
    AutomaticPressure,
    AutomaticReset,
    PrecisionReset,
    ShotgunCommit,
    PistolRhythm
}

/// <summary>
/// BOT 8A-8C combat execution and movement humanization.
///
/// This layer deliberately sits underneath BOT 7D's strategic choice. It does
/// not invent hidden information and it does not bypass OMR locomotion/weapons.
/// It converts doctrine into short combat rhythms: committed automatic bursts,
/// post-shot precision resets, close-range shotgun commits and less robotic path
/// steering. All physical movement still goes through PlayerMovementInput.
/// </summary>
public sealed partial class OMRBotController
{
    [Property, Group( "BOT 8 / Combat" )]
    public bool WeaponCombatSequencesEnabled { get; set; } = true;

    [Property, Group( "BOT 8 / Movement" )]
    public bool HumanizedMovementEnabled { get; set; } = true;

    [Property, Group( "BOT 8 / Automatic Fire" ), Range( 0.02f, 0.30f )]
    public float AutomaticBurstPauseMin { get; set; } = 0.065f;

    [Property, Group( "BOT 8 / Automatic Fire" ), Range( 0.04f, 0.45f )]
    public float AutomaticBurstPauseMax { get; set; } = 0.18f;

    [Property, Group( "BOT 8 / Precision" ), Range( 0.10f, 1.25f )]
    public float PrecisionPostShotResetScale { get; set; } = 0.78f;

    [Property, Group( "BOT 8 / Shotgun" ), Range( 0.20f, 1.5f )]
    public float ShotgunCommitDuration { get; set; } = 0.72f;

    [Property, Group( "BOT 8 / Movement" ), Range( 0f, 0.8f )]
    public float CombatInputReversalPenalty { get; set; } = 0.30f;

    [Property, Group( "BOT 8 / Movement" ), Range( 36f, 140f )]
    public float CorridorCenteringProbeDistance { get; set; } = 82f;

    [Property, Group( "BOT 8 / Movement" ), Range( 0f, 0.8f )]
    public float CorridorCenteringStrength { get; set; } = 0.34f;

    [Property, Group( "BOT 8 / Movement" ), Range( 0.04f, 0.35f )]
    public float CorridorProbeInterval { get; set; } = 0.12f;

    [Property, Sync( SyncFlags.FromHost )]
    public string CombatSequenceStatus { get; set; } = "IDLE";

    [Property, Sync( SyncFlags.FromHost )]
    public string HumanizationStatus { get; set; } = "READY";

    private OMRBotCombatSequenceKind _combatSequence = OMRBotCombatSequenceKind.None;
    private OneMoreRoundWeapon _combatSequenceWeapon;
    private int _automaticBurstShotsRemaining;
    private float _automaticBurstPauseUntil = -999f;
    private float _precisionResetUntil = -999f;
    private float _shotgunCommitUntil = -999f;
    private float _pistolRhythmUntil = -999f;
    private float _lastSequenceShotAt = -999f;

    private float _nextCorridorProbeAt;
    private float _corridorSteeringBias;

    private void ResetCombatHumanization()
    {
        _combatSequence = OMRBotCombatSequenceKind.None;
        _combatSequenceWeapon = null;
        _automaticBurstShotsRemaining = 0;
        _automaticBurstPauseUntil = -999f;
        _precisionResetUntil = -999f;
        _shotgunCommitUntil = -999f;
        _pistolRhythmUntil = -999f;
        _lastSequenceShotAt = -999f;
        _nextCorridorProbeAt = 0f;
        _corridorSteeringBias = 0f;
        CombatSequenceStatus = "IDLE";
        HumanizationStatus = "READY";
    }

    private void RefreshCombatSequenceContext( OneMoreRoundWeapon weapon )
    {
        if ( !WeaponCombatSequencesEnabled || weapon?.IsValid != true )
        {
            _combatSequence = OMRBotCombatSequenceKind.None;
            _combatSequenceWeapon = weapon;
            _automaticBurstShotsRemaining = 0;
            CombatSequenceStatus = WeaponCombatSequencesEnabled ? "UNARMED" : "OFF";
            return;
        }

        if ( _combatSequenceWeapon != weapon )
        {
            _combatSequenceWeapon = weapon;
            _automaticBurstShotsRemaining = 0;
            _automaticBurstPauseUntil = -999f;
            _precisionResetUntil = -999f;
            _shotgunCommitUntil = -999f;
            _pistolRhythmUntil = -999f;
            _combatSequence = OMRBotCombatSequenceKind.None;
        }

        OMRBotWeaponDoctrine doctrine = GetWeaponDoctrine( weapon );
        float distance = _hasConfirmedSight
            ? ( _observedTargetPosition - WorldPosition ).WithZ( 0f ).Length
            : float.PositiveInfinity;

        if (
            doctrine.Archetype == OMRWeaponArchetype.Shotgun &&
            _hasConfirmedSight &&
            distance > doctrine.LegacyHoldDistance * 0.92f &&
            distance <= doctrine.LegacyMaximumFireDistance * 1.05f
        )
        {
            _shotgunCommitUntil = MathF.Max(
                _shotgunCommitUntil,
                Time.Now + MathF.Max( 0.20f, ShotgunCommitDuration ) * 0.45f
            );
        }

        if ( Time.Now < _precisionResetUntil )
        {
            _combatSequence = OMRBotCombatSequenceKind.PrecisionReset;
            CombatSequenceStatus = $"PRECISION RESET · {MathF.Max( 0f, _precisionResetUntil - Time.Now ):0.00}s";
        }
        else if ( Time.Now < _automaticBurstPauseUntil )
        {
            _combatSequence = OMRBotCombatSequenceKind.AutomaticReset;
            CombatSequenceStatus = $"BURST RESET · {MathF.Max( 0f, _automaticBurstPauseUntil - Time.Now ):0.00}s";
        }
        else if ( doctrine.IsAutomatic && _automaticBurstShotsRemaining > 0 )
        {
            _combatSequence = OMRBotCombatSequenceKind.AutomaticPressure;
            CombatSequenceStatus = $"AUTO PRESSURE · {_automaticBurstShotsRemaining} SHOTS";
        }
        else if ( Time.Now < _shotgunCommitUntil )
        {
            _combatSequence = OMRBotCombatSequenceKind.ShotgunCommit;
            CombatSequenceStatus = "SHOTGUN COMMIT";
        }
        else if ( Time.Now < _pistolRhythmUntil )
        {
            _combatSequence = OMRBotCombatSequenceKind.PistolRhythm;
            CombatSequenceStatus = "PRECISION RHYTHM";
        }
        else
        {
            _combatSequence = OMRBotCombatSequenceKind.None;
            CombatSequenceStatus = doctrine.DisplayName;
        }
    }

    private bool TryAllowCombatSequenceShot(
        OneMoreRoundWeapon weapon,
        float distance,
        out string holdReason
    )
    {
        holdReason = string.Empty;
        if ( !WeaponCombatSequencesEnabled || weapon?.IsValid != true )
            return true;

        OMRBotWeaponDoctrine doctrine = GetWeaponDoctrine( weapon );
        if ( !doctrine.IsAutomatic )
            return true;

        if ( Time.Now < _automaticBurstPauseUntil )
        {
            holdReason = "BURST RESET";
            return false;
        }

        if ( _automaticBurstShotsRemaining <= 0 )
            BeginAutomaticBurst( doctrine, distance );

        return _automaticBurstShotsRemaining > 0;
    }

    private void BeginAutomaticBurst( OMRBotWeaponDoctrine doctrine, float distance )
    {
        int minShots;
        int maxShots;

        if ( doctrine.WeaponId == "mp5" )
        {
            minShots = 7;
            maxShots = 13;
        }
        else if ( doctrine.WeaponId == "m4a1" )
        {
            minShots = distance > doctrine.LegacyHoldDistance * 1.25f ? 3 : 5;
            maxShots = distance > doctrine.LegacyHoldDistance * 1.25f ? 6 : 9;
        }
        else
        {
            minShots = 4;
            maxShots = 8;
        }

        if ( _hasConfirmedSight && Difficulty != OMRBotDifficulty.Easy &&
            distance <= doctrine.LegacyHoldDistance * 1.25f && CombatAimError <= GetFireToleranceDegrees() * 1.25f )
        {
            minShots += 3;
            maxShots += 5;
        }

        if ( Difficulty == OMRBotDifficulty.Easy )
        {
            minShots = Math.Max( 2, minShots - 2 );
            maxShots = Math.Max( minShots, maxShots - 2 );
        }
        else if ( Difficulty == OMRBotDifficulty.Hard )
        {
            maxShots += 2;
        }

        float sustained = MathX.Clamp( doctrine.SustainedFireBias, 0f, 1f );
        int weightedMax = Math.Max(
            minShots,
            (int)MathF.Round( MathX.Lerp( minShots, maxShots, sustained ) )
        );

        _automaticBurstShotsRemaining = Game.Random.Int( minShots, weightedMax );
        _combatSequence = OMRBotCombatSequenceKind.AutomaticPressure;
        CombatSequenceStatus = $"AUTO PRESSURE · {_automaticBurstShotsRemaining} SHOTS";
    }

    private float GetCombatSequenceTriggerInterval( OneMoreRoundWeapon weapon )
    {
        if ( weapon?.IsValid == true && weapon.IsAutomaticWeapon ) return 0f;
        return GetBotTriggerInterval( weapon );
    }

    private void OnCombatSequenceShotFired( OneMoreRoundWeapon weapon, float distance )
    {
        if ( !WeaponCombatSequencesEnabled || weapon?.IsValid != true )
            return;

        _lastSequenceShotAt = Time.Now;
        OMRBotWeaponDoctrine doctrine = GetWeaponDoctrine( weapon );

        if ( doctrine.IsAutomatic )
        {
            _automaticBurstShotsRemaining = Math.Max( 0, _automaticBurstShotsRemaining - 1 );
            if ( _automaticBurstShotsRemaining <= 0 )
            {
                float minPause = MathF.Max( 0.02f, AutomaticBurstPauseMin );
                float maxPause = MathF.Max( minPause, AutomaticBurstPauseMax );
                float discipline = MathX.Clamp( doctrine.BurstDisciplineBias, 0f, 1f );
                float pause = Game.Random.Float( minPause, maxPause );
                pause *= MathX.Lerp( 0.65f, 1.25f, discipline );
                if ( Difficulty == OMRBotDifficulty.Hard ) pause *= 0.82f;
                if ( Difficulty == OMRBotDifficulty.Easy ) pause *= 1.18f;
                _automaticBurstPauseUntil = Time.Now + pause;
                _combatSequence = OMRBotCombatSequenceKind.AutomaticReset;
            }
            else
            {
                _combatSequence = OMRBotCombatSequenceKind.AutomaticPressure;
            }

            // Do not let a healthy automatic-fire engagement get interrupted by a
            // trivially different utility score halfway through a committed burst.
            if (
                _utilitySelectedIntent == OMRBotIntentKind.Engage ||
                _utilitySelectedIntent == OMRBotIntentKind.Press
            )
            {
                _utilityCommitUntil = MathF.Max( _utilityCommitUntil, Time.Now + 0.26f );
            }
            return;
        }

        if ( doctrine.PostShotResetBias >= 0.72f )
        {
            float reset = MathF.Max(
                0.18f,
                doctrine.FireInterval * MathF.Max( 0.25f, PrecisionPostShotResetScale )
            );
            reset *= MathX.Lerp( 0.70f, 1.10f, doctrine.PostShotResetBias );
            _precisionResetUntil = Time.Now + reset;
            _combatSequence = OMRBotCombatSequenceKind.PrecisionReset;

            // If the shot came from an authored cover peek, immediately begin the
            // existing return-to-anchor phase. The next peek naturally alternates.
            if ( _coverPeekActive && !_coverPeekReturning )
            {
                _coverPeekReturning = true;
                _coverPeekUntil = -1f;
            }
            _nextUtilityDecisionAt = 0f;
            return;
        }

        if ( doctrine.Archetype == OMRWeaponArchetype.Shotgun )
        {
            _shotgunCommitUntil = Time.Now + MathF.Max( 0.20f, ShotgunCommitDuration );
            _combatSequence = OMRBotCombatSequenceKind.ShotgunCommit;
            _nextUtilityDecisionAt = 0f;
            return;
        }

        if ( doctrine.Archetype == OMRWeaponArchetype.Pistol )
        {
            _pistolRhythmUntil = Time.Now + MathF.Max( 0.12f, doctrine.FireInterval * 0.65f );
            _combatSequence = OMRBotCombatSequenceKind.PistolRhythm;
        }
    }

    private float GetCombatSequenceUtilityAdjustment( OMRBotIntentKind intent )
    {
        if ( !WeaponCombatSequencesEnabled )
            return 0f;

        OMRBotWeaponDoctrine doctrine = _brainWeaponDoctrine ?? OMRBotWeaponDoctrine.Empty;
        float adjustment = 0f;

        bool autoPressure =
            _combatSequence == OMRBotCombatSequenceKind.AutomaticPressure &&
            _automaticBurstShotsRemaining > 0;
        bool precisionReset = Time.Now < _precisionResetUntil;
        bool shotgunCommit = Time.Now < _shotgunCommitUntil;
        bool pistolRhythm = Time.Now < _pistolRhythmUntil;

        if ( autoPressure )
        {
            if ( intent == OMRBotIntentKind.Engage ) adjustment += 0.20f;
            if ( intent == OMRBotIntentKind.Press ) adjustment += 0.10f + doctrine.AggressiveCommitBias * 0.05f;
            if ( intent == OMRBotIntentKind.Reposition ) adjustment -= 0.09f;
            if ( intent == OMRBotIntentKind.TakeCover ) adjustment -= 0.10f;
            if ( intent == OMRBotIntentKind.FlankLeft || intent == OMRBotIntentKind.FlankRight ) adjustment -= 0.05f;
        }

        if ( precisionReset )
        {
            if ( intent == OMRBotIntentKind.TakeCover ) adjustment += 0.42f * doctrine.PostShotResetBias;
            if ( intent == OMRBotIntentKind.Reposition ) adjustment += 0.28f * doctrine.PostShotResetBias;
            if ( intent == OMRBotIntentKind.OpenDistance ) adjustment += 0.18f * doctrine.PostShotResetBias;
            if ( intent == OMRBotIntentKind.Engage ) adjustment -= 0.32f * doctrine.PostShotResetBias;
            if ( intent == OMRBotIntentKind.Press ) adjustment -= 0.38f * doctrine.PostShotResetBias;
        }

        if ( shotgunCommit )
        {
            if ( intent == OMRBotIntentKind.Press ) adjustment += 0.20f + doctrine.AggressiveCommitBias * 0.16f;
            if ( intent == OMRBotIntentKind.Engage ) adjustment += 0.10f + doctrine.AggressiveCommitBias * 0.05f;
            if ( intent == OMRBotIntentKind.FlankLeft || intent == OMRBotIntentKind.FlankRight ) adjustment += 0.07f;
            if ( intent == OMRBotIntentKind.OpenDistance ) adjustment -= 0.32f;
            if ( intent == OMRBotIntentKind.TakeCover ) adjustment -= 0.10f;
        }

        if ( pistolRhythm )
        {
            if ( intent == OMRBotIntentKind.Engage ) adjustment += 0.05f;
            if ( intent == OMRBotIntentKind.Reposition ) adjustment += 0.035f;
        }

        return adjustment;
    }

    private void ApplyCombatSequenceMovementScore(
        Vector3 localInput,
        OMRBotWeaponDoctrine doctrine,
        float currentDistance,
        float holdDistance,
        ref float score
    )
    {
        if ( !WeaponCombatSequencesEnabled || doctrine is null )
            return;

        float forward = localInput.x;
        float lateral = MathF.Abs( localInput.y );
        float amount = localInput.WithZ( 0f ).Length;

        if ( _combatSequence == OMRBotCombatSequenceKind.AutomaticPressure )
        {
            score += lateral * doctrine.SustainedFireBias * 0.12f;
            if ( amount <= 0.01f ) score += doctrine.CombatStabilityBias * 0.06f;
        }

        if ( Time.Now < _precisionResetUntil )
        {
            if ( forward < -0.05f ) score += 0.32f * doctrine.PostShotResetBias;
            if ( lateral > 0.30f ) score += 0.18f * doctrine.PostShotResetBias;
            if ( forward > 0.05f ) score -= 0.34f * doctrine.PostShotResetBias;
        }

        if ( Time.Now < _shotgunCommitUntil )
        {
            if ( currentDistance > holdDistance * 0.82f && forward > 0.05f ) score += 0.38f;
            if ( currentDistance > holdDistance && forward < -0.05f ) score -= 0.42f;
            score += lateral * 0.06f;
        }
    }

    private bool TryHoldPrecisionResetInCover( OneMoreRoundWeapon weapon )
    {
        if (
            !WeaponCombatSequencesEnabled ||
            weapon?.IsValid != true ||
            Time.Now >= _precisionResetUntil ||
            !_hasCoverTarget ||
            _coverPeekActive
        )
            return false;

        OMRBotWeaponDoctrine doctrine = GetWeaponDoctrine( weapon );
        if ( doctrine.PostShotResetBias < 0.72f )
            return false;

        EnsureBotCrouchState( _coverPosture == OMRCoverPosture.Crouch );
        StopMovementOnly();
        ReleaseBotWeaponIntent( weapon );
        CombatWantsAim = false;
        CoverStatus = weapon.IsCyclePendingOrActive
            ? "RESET / CYCLING IN COVER"
            : "RESET / HIDDEN";
        TacticalStatus = "POST-SHOT RESET";
        CombatStatus = weapon.IsCyclePendingOrActive ? "CYCLING IN COVER" : "RESETTING";
        return true;
    }

    private float GetHumanizedCombatDecisionIntervalScale( OMRBotWeaponDoctrine doctrine )
    {
        if ( !HumanizedMovementEnabled || doctrine is null )
            return 1f;

        float scale = MathX.Lerp( 0.94f, 1.16f, doctrine.CombatStabilityBias );
        if ( _combatSequence == OMRBotCombatSequenceKind.AutomaticPressure )
            scale *= 0.92f;
        if ( Time.Now < _precisionResetUntil )
            scale *= 1.08f;
        return scale;
    }

    private bool ApplyHumanizedCombatCandidateScore(
        Vector3 localInput,
        float wallClearance,
        ref float score
    )
    {
        if ( !HumanizedMovementEnabled )
            return true;

        Vector3 previous = _combatCommittedInput.WithZ( 0f );
        Vector3 proposed = localInput.WithZ( 0f );
        if ( !previous.IsNearlyZero( 0.01f ) && !proposed.IsNearlyZero( 0.01f ) )
        {
            float directionalDot = previous.Normal.Dot( proposed.Normal );
            if ( directionalDot < -0.20f )
                score -= MathF.Max( 0f, CombatInputReversalPenalty ) * MathF.Abs( directionalDot );
            else if ( directionalDot > 0.72f )
                score += 0.055f;
        }

        // The existing four-direction clearance probe is already paid for by BOT
        // 7B. Use it more decisively instead of adding another per-candidate trace
        // batch. This rejects the classic AI behavior of deliberately walking with
        // its shoulder almost scraping arena walls.
        if ( !proposed.IsNearlyZero( 0.01f ) )
        {
            float clearance = wallClearance;
            float minimum = Difficulty switch
            {
                OMRBotDifficulty.Easy => 0.09f,
                OMRBotDifficulty.Hard => 0.15f,
                _ => 0.12f
            };
            if ( clearance < minimum )
                return false;
            score += clearance * 0.10f;
        }

        return true;
    }

    private Vector3 ApplyHumanizedPathSteering( Vector3 desired )
    {
        if (
            !HumanizedMovementEnabled ||
            Scene is null ||
            desired.WithZ( 0f ).IsNearlyZero( 0.01f )
        )
        {
            HumanizationStatus = HumanizedMovementEnabled ? "READY" : "OFF";
            return desired;
        }

        Vector3 forward = desired.WithZ( 0f ).Normal;
        Vector3 lateral = new Vector3( -forward.y, forward.x, 0f ).Normal;

        if ( Time.Now >= _nextCorridorProbeAt )
        {
            _nextCorridorProbeAt = Time.Now + MathF.Max( 0.04f, CorridorProbeInterval );
            float probe = MathF.Max( 36f, CorridorCenteringProbeDistance );
            float leftDistance = ProbeHumanizedWallDistance( lateral, probe );
            float rightDistance = ProbeHumanizedWallDistance( -lateral, probe );
            float leftPressure = 1f - MathX.Clamp( leftDistance / probe, 0f, 1f );
            float rightPressure = 1f - MathX.Clamp( rightDistance / probe, 0f, 1f );
            _corridorSteeringBias = MathX.Clamp( rightPressure - leftPressure, -1f, 1f );
        }

        float strength = MathF.Max( 0f, CorridorCenteringStrength );
        if ( MathF.Abs( _corridorSteeringBias ) <= 0.025f || strength <= 0f )
        {
            HumanizationStatus = "CENTERED";
            return forward;
        }

        Vector3 adjusted = ( forward + lateral * _corridorSteeringBias * strength ).WithZ( 0f );
        if ( adjusted.IsNearlyZero( 0.01f ) )
            return forward;

        HumanizationStatus = _corridorSteeringBias > 0f ? "CENTER / LEFT" : "CENTER / RIGHT";
        return adjusted.Normal;
    }

    private float ProbeHumanizedWallDistance( Vector3 direction, float maximum )
    {
        Vector3 start = WorldPosition + Vector3.Up * 34f;
        SceneTrace trace =
            Scene.Trace.Ray( start, start + direction.WithZ( 0f ).Normal * maximum )
                .IgnoreGameObjectHierarchy( GameObject )
                .WithoutTags( "trigger" )
                .UseHitboxes( false );

        if ( _perceptionTarget?.GameObject is not null )
            trace = trace.IgnoreGameObjectHierarchy( _perceptionTarget.GameObject );

        SceneTraceResult result = trace.Run();
        if ( !result.Hit )
            return maximum;

        return MathF.Min( maximum, ( result.EndPosition - start ).Length );
    }
}