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();
}
}