OMRWeaponModifiers.cs
using Sandbox;
using System;
using System.Collections.Generic;

/// <summary>
/// Numeric stats that can be changed by a reusable weapon modifier.
/// Vector and boolean values are kept in separate enums so the Inspector stays
/// strongly typed instead of encoding arbitrary values as strings.
/// </summary>
public enum OMRWeaponScalarStat
{
	Damage,
	Pellets,
	Range,
	Radius,
	Force,
	SpreadRecovery,
	FireInterval,
	MagazineSize,
	ReloadTime,
	DeployTime,
	IronSightsFireScale,
	AimFovScale,
	AimFovTransitionTime,
	AimAnimationSpeed,
	SprintToFireDelay,
	SprintFirePoseHoldTime,
	MovingSpeedThreshold,
	MovingSpreadMultiplier,
	CrouchedSpreadMultiplier,
	AirborneSpreadMultiplier,
	SlidingSpreadMultiplier,
	AimSpreadMultiplier,
	CameraRecoilRoll,
	CameraRecoilFrequency,
	CameraRecoilDuration,
	AdsCameraRecoilScale,
	CrosshairSpreadVisualScale,
	CrosshairMinimumGap,
	CrosshairMaximumGap,
	CrosshairLineLength,
	CrosshairLineWidth,

	// Added after the original Phase 1B stat set so serialized enum ordinals
	// from existing modifier assets remain stable.
	MovementAccuracyStartFraction,
	MovementAccuracyFullFraction,
	RecoilPatternResetDelay,
	RecoilRecoveryFraction,
	RecoilRecoveryDelay,
	RecoilRecoveryPitchSpeed,
	RecoilRecoveryYawSpeed
}

public enum OMRWeaponVectorStat
{
	SpreadBase,
	SpreadGrowth,
	RecoilPitch,
	RecoilYaw,
	RecoilPatternNoise,

	// Added after Phase 3B so existing serialized enum ordinals remain stable.
	RecoilPatternScale
}

public enum OMRWeaponBoolStat
{
	PrimaryAutomatic,
	AutoReload,
	CanCancelReload,
	RecoilPatternLoop,
	RecoilRecoveryEnabled
}

public enum OMRWeaponModifierOperation
{
	Add,
	Multiply,
	Override
}

/// <summary>
/// One scalar operation inside an authored modifier.
/// For integer-backed stats such as Pellets/MagazineSize, the final resolved
/// value is rounded and normalized after the full modifier stack has run.
/// </summary>
public sealed class OMRWeaponScalarOperation
{
	[Property]
	public OMRWeaponScalarStat Stat { get; set; }

	[Property]
	public OMRWeaponModifierOperation Operation { get; set; } = OMRWeaponModifierOperation.Multiply;

	[Property]
	public float Value { get; set; } = 1f;
}

/// <summary>
/// One Vector2 operation. Multiply is component-wise, so 1.2,1.2 scales both
/// axes equally while 1.2,0.8 can intentionally treat axes differently.
/// </summary>
public sealed class OMRWeaponVectorOperation
{
	[Property]
	public OMRWeaponVectorStat Stat { get; set; }

	[Property]
	public OMRWeaponModifierOperation Operation { get; set; } = OMRWeaponModifierOperation.Multiply;

	[Property]
	public Vector2 Value { get; set; } = Vector2.One;
}

/// <summary>
/// Boolean stats only support an explicit override. Treating booleans as
/// multiply/add values would be clever in precisely the wrong way.
/// </summary>
public sealed class OMRWeaponBoolOverride
{
	[Property]
	public OMRWeaponBoolStat Stat { get; set; }

	[Property]
	public bool Value { get; set; }
}

/// <summary>
/// Generic runtime contract for anything capable of transforming weapon stats.
/// OMR round rules, future attachments, perks, powerups or another S&box game
/// can implement this without the weapon core knowing where the modifier came
/// from.
/// </summary>
public interface IOMRWeaponStatModifier
{
	string ModifierId { get; }
	int Priority { get; }
	void Apply( OMRWeaponStats stats );
}

/// <summary>
/// Inspector-authored reusable modifier asset. The source weapon definition is
/// never changed; this asset only transforms a cloned OMRWeaponStats snapshot.
/// </summary>
[AssetType(
	Name = "OMR Weapon Modifier",
	Extension = "omrmod",
	Category = "One More Round/Weapons"
)]
public sealed partial class OMRWeaponModifierDefinition : GameResource, IOMRWeaponStatModifier
{
	[Group( "Identity" ), Title( "Modifier ID" ), Description( "Stable unique ID within a weapon's active modifier stack." )]
	public string ModifierId { get; set; } = string.Empty;

	[Group( "Identity" ), Description( "Lower priorities resolve first. Equal priorities are ordered by Modifier ID for deterministic results." )]
	public int Priority { get; set; } = 100;

	[Group( "Operations" )]
	public List<OMRWeaponScalarOperation> ScalarOperations { get; set; } = new();

	[Group( "Operations" )]
	public List<OMRWeaponVectorOperation> VectorOperations { get; set; } = new();

	[Group( "Operations" )]
	public List<OMRWeaponBoolOverride> BoolOverrides { get; set; } = new();

	public void Apply( OMRWeaponStats stats )
	{
		if ( stats is null )
			return;

		if ( ScalarOperations is not null )
		{
			foreach ( OMRWeaponScalarOperation operation in ScalarOperations )
			{
				if ( operation is null )
					continue;

				ApplyScalarOperation( stats, operation );
			}
		}

		if ( VectorOperations is not null )
		{
			foreach ( OMRWeaponVectorOperation operation in VectorOperations )
			{
				if ( operation is null )
					continue;

				ApplyVectorOperation( stats, operation );
			}
		}

		if ( BoolOverrides is not null )
		{
			foreach ( OMRWeaponBoolOverride operation in BoolOverrides )
			{
				if ( operation is null )
					continue;

				ApplyBoolOverride( stats, operation );
			}
		}
	}

	private static void ApplyScalarOperation( OMRWeaponStats stats, OMRWeaponScalarOperation operation )
	{
		float current = GetScalar( stats, operation.Stat );
		float resolved = ApplyFloatOperation( current, operation.Value, operation.Operation );
		SetScalar( stats, operation.Stat, resolved );
	}

	private static void ApplyVectorOperation( OMRWeaponStats stats, OMRWeaponVectorOperation operation )
	{
		Vector2 current = GetVector( stats, operation.Stat );
		Vector2 resolved = operation.Operation switch
		{
			OMRWeaponModifierOperation.Add => current + operation.Value,
			OMRWeaponModifierOperation.Multiply => new Vector2(
				current.x * operation.Value.x,
				current.y * operation.Value.y
			),
			OMRWeaponModifierOperation.Override => operation.Value,
			_ => current
		};

		SetVector( stats, operation.Stat, resolved );
	}

	private static void ApplyBoolOverride( OMRWeaponStats stats, OMRWeaponBoolOverride operation )
	{
		switch ( operation.Stat )
		{
			case OMRWeaponBoolStat.PrimaryAutomatic:
				stats.PrimaryAutomatic = operation.Value;
				break;
			case OMRWeaponBoolStat.AutoReload:
				stats.AutoReload = operation.Value;
				break;
			case OMRWeaponBoolStat.CanCancelReload:
				stats.CanCancelReload = operation.Value;
				break;
			case OMRWeaponBoolStat.RecoilPatternLoop:
				stats.RecoilPatternLoop = operation.Value;
				break;
			case OMRWeaponBoolStat.RecoilRecoveryEnabled:
				stats.RecoilRecoveryEnabled = operation.Value;
				break;
		}
	}

	private static float ApplyFloatOperation( float current, float value, OMRWeaponModifierOperation operation )
	{
		return operation switch
		{
			OMRWeaponModifierOperation.Add => current + value,
			OMRWeaponModifierOperation.Multiply => current * value,
			OMRWeaponModifierOperation.Override => value,
			_ => current
		};
	}

	private static float GetScalar( OMRWeaponStats stats, OMRWeaponScalarStat stat )
	{
		return stat switch
		{
			OMRWeaponScalarStat.Damage => stats.Damage,
			OMRWeaponScalarStat.Pellets => stats.Pellets,
			OMRWeaponScalarStat.Range => stats.Range,
			OMRWeaponScalarStat.Radius => stats.Radius,
			OMRWeaponScalarStat.Force => stats.Force,
			OMRWeaponScalarStat.SpreadRecovery => stats.SpreadRecovery,
			OMRWeaponScalarStat.FireInterval => stats.PrimaryDelay,
			OMRWeaponScalarStat.MagazineSize => stats.ClipMaxSize,
			OMRWeaponScalarStat.ReloadTime => stats.ReloadTime,
			OMRWeaponScalarStat.DeployTime => stats.DeployTime,
			OMRWeaponScalarStat.IronSightsFireScale => stats.IronSightsFireScale,
			OMRWeaponScalarStat.AimFovScale => stats.AimFovScale,
			OMRWeaponScalarStat.AimFovTransitionTime => stats.AimFovTransitionTime,
			OMRWeaponScalarStat.AimAnimationSpeed => stats.AimAnimationSpeed,
			OMRWeaponScalarStat.SprintToFireDelay => stats.SprintToFireDelay,
			OMRWeaponScalarStat.SprintFirePoseHoldTime => stats.SprintFirePoseHoldTime,
			OMRWeaponScalarStat.MovingSpeedThreshold => stats.MovingSpeedThreshold,
			OMRWeaponScalarStat.MovementAccuracyStartFraction => stats.MovementAccuracyStartFraction,
			OMRWeaponScalarStat.MovementAccuracyFullFraction => stats.MovementAccuracyFullFraction,
			OMRWeaponScalarStat.MovingSpreadMultiplier => stats.MovingSpreadMultiplier,
			OMRWeaponScalarStat.CrouchedSpreadMultiplier => stats.CrouchedSpreadMultiplier,
			OMRWeaponScalarStat.AirborneSpreadMultiplier => stats.AirborneSpreadMultiplier,
			OMRWeaponScalarStat.SlidingSpreadMultiplier => stats.SlidingSpreadMultiplier,
			OMRWeaponScalarStat.AimSpreadMultiplier => stats.AimSpreadMultiplier,
			OMRWeaponScalarStat.CameraRecoilRoll => stats.CameraRecoilRoll,
			OMRWeaponScalarStat.CameraRecoilFrequency => stats.CameraRecoilFrequency,
			OMRWeaponScalarStat.CameraRecoilDuration => stats.CameraRecoilDuration,
			OMRWeaponScalarStat.AdsCameraRecoilScale => stats.AdsCameraRecoilScale,
			OMRWeaponScalarStat.RecoilPatternResetDelay => stats.RecoilPatternResetDelay,
			OMRWeaponScalarStat.RecoilRecoveryFraction => stats.RecoilRecoveryFraction,
			OMRWeaponScalarStat.RecoilRecoveryDelay => stats.RecoilRecoveryDelay,
			OMRWeaponScalarStat.RecoilRecoveryPitchSpeed => stats.RecoilRecoveryPitchSpeed,
			OMRWeaponScalarStat.RecoilRecoveryYawSpeed => stats.RecoilRecoveryYawSpeed,
			OMRWeaponScalarStat.CrosshairSpreadVisualScale => stats.CrosshairSpreadVisualScale,
			OMRWeaponScalarStat.CrosshairMinimumGap => stats.CrosshairMinimumGap,
			OMRWeaponScalarStat.CrosshairMaximumGap => stats.CrosshairMaximumGap,
			OMRWeaponScalarStat.CrosshairLineLength => stats.CrosshairLineLength,
			OMRWeaponScalarStat.CrosshairLineWidth => stats.CrosshairLineWidth,
			_ => 0f
		};
	}

	private static void SetScalar( OMRWeaponStats stats, OMRWeaponScalarStat stat, float value )
	{
		switch ( stat )
		{
			case OMRWeaponScalarStat.Damage: stats.Damage = value; break;
			case OMRWeaponScalarStat.Pellets: stats.Pellets = (int)MathF.Round( value ); break;
			case OMRWeaponScalarStat.Range: stats.Range = value; break;
			case OMRWeaponScalarStat.Radius: stats.Radius = value; break;
			case OMRWeaponScalarStat.Force: stats.Force = value; break;
			case OMRWeaponScalarStat.SpreadRecovery: stats.SpreadRecovery = value; break;
			case OMRWeaponScalarStat.FireInterval: stats.PrimaryDelay = value; break;
			case OMRWeaponScalarStat.MagazineSize: stats.ClipMaxSize = (int)MathF.Round( value ); break;
			case OMRWeaponScalarStat.ReloadTime: stats.ReloadTime = value; break;
			case OMRWeaponScalarStat.DeployTime: stats.DeployTime = value; break;
			case OMRWeaponScalarStat.IronSightsFireScale: stats.IronSightsFireScale = value; break;
			case OMRWeaponScalarStat.AimFovScale: stats.AimFovScale = value; break;
			case OMRWeaponScalarStat.AimFovTransitionTime: stats.AimFovTransitionTime = value; break;
			case OMRWeaponScalarStat.AimAnimationSpeed: stats.AimAnimationSpeed = value; break;
			case OMRWeaponScalarStat.SprintToFireDelay: stats.SprintToFireDelay = value; break;
			case OMRWeaponScalarStat.SprintFirePoseHoldTime: stats.SprintFirePoseHoldTime = value; break;
			case OMRWeaponScalarStat.MovingSpeedThreshold: stats.MovingSpeedThreshold = value; break;
			case OMRWeaponScalarStat.MovementAccuracyStartFraction: stats.MovementAccuracyStartFraction = value; break;
			case OMRWeaponScalarStat.MovementAccuracyFullFraction: stats.MovementAccuracyFullFraction = value; break;
			case OMRWeaponScalarStat.MovingSpreadMultiplier: stats.MovingSpreadMultiplier = value; break;
			case OMRWeaponScalarStat.CrouchedSpreadMultiplier: stats.CrouchedSpreadMultiplier = value; break;
			case OMRWeaponScalarStat.AirborneSpreadMultiplier: stats.AirborneSpreadMultiplier = value; break;
			case OMRWeaponScalarStat.SlidingSpreadMultiplier: stats.SlidingSpreadMultiplier = value; break;
			case OMRWeaponScalarStat.AimSpreadMultiplier: stats.AimSpreadMultiplier = value; break;
			case OMRWeaponScalarStat.CameraRecoilRoll: stats.CameraRecoilRoll = value; break;
			case OMRWeaponScalarStat.CameraRecoilFrequency: stats.CameraRecoilFrequency = value; break;
			case OMRWeaponScalarStat.CameraRecoilDuration: stats.CameraRecoilDuration = value; break;
			case OMRWeaponScalarStat.AdsCameraRecoilScale: stats.AdsCameraRecoilScale = value; break;
			case OMRWeaponScalarStat.RecoilPatternResetDelay: stats.RecoilPatternResetDelay = value; break;
			case OMRWeaponScalarStat.RecoilRecoveryFraction: stats.RecoilRecoveryFraction = value; break;
			case OMRWeaponScalarStat.RecoilRecoveryDelay: stats.RecoilRecoveryDelay = value; break;
			case OMRWeaponScalarStat.RecoilRecoveryPitchSpeed: stats.RecoilRecoveryPitchSpeed = value; break;
			case OMRWeaponScalarStat.RecoilRecoveryYawSpeed: stats.RecoilRecoveryYawSpeed = value; break;
			case OMRWeaponScalarStat.CrosshairSpreadVisualScale: stats.CrosshairSpreadVisualScale = value; break;
			case OMRWeaponScalarStat.CrosshairMinimumGap: stats.CrosshairMinimumGap = value; break;
			case OMRWeaponScalarStat.CrosshairMaximumGap: stats.CrosshairMaximumGap = value; break;
			case OMRWeaponScalarStat.CrosshairLineLength: stats.CrosshairLineLength = value; break;
			case OMRWeaponScalarStat.CrosshairLineWidth: stats.CrosshairLineWidth = value; break;
		}
	}

	private static Vector2 GetVector( OMRWeaponStats stats, OMRWeaponVectorStat stat )
	{
		return stat switch
		{
			OMRWeaponVectorStat.SpreadBase => stats.SpreadBase,
			OMRWeaponVectorStat.SpreadGrowth => stats.SpreadGrowth,
			OMRWeaponVectorStat.RecoilPitch => stats.RecoilPitch,
			OMRWeaponVectorStat.RecoilYaw => stats.RecoilYaw,
			OMRWeaponVectorStat.RecoilPatternNoise => stats.RecoilPatternNoise,
			OMRWeaponVectorStat.RecoilPatternScale => stats.RecoilPatternScale,
			_ => Vector2.Zero
		};
	}

	private static void SetVector( OMRWeaponStats stats, OMRWeaponVectorStat stat, Vector2 value )
	{
		switch ( stat )
		{
			case OMRWeaponVectorStat.SpreadBase: stats.SpreadBase = value; break;
			case OMRWeaponVectorStat.SpreadGrowth: stats.SpreadGrowth = value; break;
			case OMRWeaponVectorStat.RecoilPitch: stats.RecoilPitch = value; break;
			case OMRWeaponVectorStat.RecoilYaw: stats.RecoilYaw = value; break;
			case OMRWeaponVectorStat.RecoilPatternNoise: stats.RecoilPatternNoise = value; break;
			case OMRWeaponVectorStat.RecoilPatternScale: stats.RecoilPatternScale = value; break;
		}
	}
}

/// <summary>
/// Optional bridge that applies authored modifier resources to a weapon. It is
/// useful for testing, pickups, attachments or simple prefab-authored effects.
/// OMR's round rules do not need to use this component; they can call the same
/// SetStatModifier/RemoveStatModifier API directly.
/// </summary>
public sealed class OMRWeaponModifierSource : Component
{
	[Property]
	public OneMoreRoundWeapon Weapon { get; set; }

	[Property]
	public List<OMRWeaponModifierDefinition> Modifiers { get; set; } = new();

	private readonly HashSet<string> _registeredModifierIds = new( StringComparer.OrdinalIgnoreCase );

	protected override void OnStart()
	{
		Weapon ??= Components.Get<OneMoreRoundWeapon>( FindMode.InSelf | FindMode.InParent | FindMode.InAncestors );
		ApplyModifiers();
	}

	protected override void OnDisabled()
	{
		RemoveModifiers();
		base.OnDisabled();
	}

	[Button, Title( "Apply Modifiers" )]
	public void ApplyModifiers()
	{
		if ( Weapon is null || !Weapon.IsValid )
			return;

		if ( Modifiers is null )
			return;

		foreach ( OMRWeaponModifierDefinition modifier in Modifiers )
		{
			if ( modifier is null || !modifier.IsValid )
				continue;

			if ( Weapon.SetStatModifier( modifier ) )
			{
				_registeredModifierIds.Add( modifier.ModifierId?.Trim() ?? string.Empty );
			}
		}
	}

	[Button, Title( "Remove Modifiers" )]
	public void RemoveModifiers()
	{
		if ( Weapon is null || !Weapon.IsValid )
		{
			_registeredModifierIds.Clear();
			return;
		}

		foreach ( string modifierId in _registeredModifierIds )
		{
			if ( string.IsNullOrWhiteSpace( modifierId ) )
				continue;

			Weapon.RemoveStatModifier( modifierId );
		}

		_registeredModifierIds.Clear();
	}
}