/ GUNRPG.Core / Weapons / Weapon.cs
Weapon.cs
  1  namespace GUNRPG.Core.Weapons;
  2  
  3  /// <summary>
  4  /// Represents weapon configuration with raw stats.
  5  /// All stats are used 1:1 without abstraction.
  6  /// </summary>
  7  public class Weapon
  8  {
  9      public string Name { get; set; }
 10      
 11      // Firing Stats (from raw weapon data)
 12      public int RoundsPerMinute { get; set; } // Fire rate
 13      public float BulletVelocityMetersPerSecond { get; set; }
 14      public int MagazineSize { get; set; }
 15      public int ReloadTimeMs { get; set; }
 16      
 17      // Damage Stats
 18      public float BaseDamage { get; set; }
 19      public float HeadshotMultiplier { get; set; }
 20  
 21      // Advanced damage model: range steps + body part multipliers.
 22      public List<DamageRange> DamageRanges { get; } = new();
 23      public Dictionary<BodyPart, float> BodyPartDamageMultipliers { get; } = new();
 24  
 25      // Optional: per range step overrides for body-part damage.
 26      // If a range defines BodyPartDamageOverrides, those values win over BaseDamage * multiplier.
 27      public float GetDamageAtDistance(float distance, BodyPart bodyPart)
 28      {
 29          if (DamageRanges.Count > 0)
 30          {
 31              var range = DamageRanges.FirstOrDefault(r => distance >= r.MinMeters && distance < r.MaxMeters)
 32                          ?? DamageRanges.Where(r => float.IsPositiveInfinity(r.MaxMeters))
 33                              .OrderByDescending(r => r.MinMeters)
 34                              .FirstOrDefault();
 35  
 36              if (range != null && range.BodyPartDamageOverrides != null &&
 37                  range.BodyPartDamageOverrides.TryGetValue(bodyPart, out float overrideDamage))
 38              {
 39                  return overrideDamage;
 40              }
 41          }
 42  
 43          float baseDamage = GetDamageAtDistance(distance, isHeadshot: bodyPart == BodyPart.Head);
 44          if (BodyPartDamageMultipliers.TryGetValue(bodyPart, out float multiplier))
 45              return baseDamage * multiplier;
 46  
 47          return baseDamage;
 48      }
 49      
 50      // Accuracy Stats
 51      public float HipfireSpreadDegrees { get; set; }
 52      public float JumpHipfireSpreadDegrees { get; set; }
 53      public float SlideHipfireSpreadDegrees { get; set; }
 54      public float DiveHipfireSpreadDegrees { get; set; }
 55      public float ADSSpreadDegrees { get; set; }
 56  
 57      // Advanced Recoil / Handling Stats (raw values)
 58      public float FirstShotRecoilScale { get; set; }
 59      public float RecoilGunKickDegreesPerSecond { get; set; }
 60      public float HorizontalRecoilControlDegreesPerSecond { get; set; }
 61      public float VerticalRecoilControlDegreesPerSecond { get; set; }
 62      public int KickResetSpeedMs { get; set; }
 63      public float AimingIdleSwayDegreesPerSecond { get; set; }
 64      public int AimingIdleSwayDelayMs { get; set; }
 65      public float FlinchResistance { get; set; }
 66  
 67      /// <summary>
 68      /// Suppression factor for this weapon class.
 69      /// Higher values = more suppressive (LMGs > ARs > SMGs).
 70      /// Typical values: SMG = 0.8, AR = 1.0, LMG = 1.5
 71      /// </summary>
 72      public float SuppressionFactor { get; set; }
 73      
 74      // Recoil Stats
 75      public float VerticalRecoil { get; set; }
 76      public float HorizontalRecoil { get; set; }
 77      public float RecoilRecoveryTimeMs { get; set; }
 78      
 79      // ADS Stats
 80      public int ADSTimeMs { get; set; } // Time to enter ADS
 81      public int JumpADSTimeMs { get; set; }
 82      public float ADSMovementSpeedMultiplier { get; set; } // Movement speed while ADS (0-1)
 83      
 84      // Movement Penalties
 85      public float SprintToFireTimeMs { get; set; } // Time to exit sprint and fire
 86      public float SlideToFireTimeMs { get; set; }
 87      public float DiveToFireTimeMs { get; set; }
 88      public float JumpSprintToFireTimeMs { get; set; }
 89  
 90      // Mobility (raw, meters/second)
 91      public float MovementSpeedMetersPerSecond { get; set; }
 92      public float CrouchMovementSpeedMetersPerSecond { get; set; }
 93      public float SprintingMovementSpeedMetersPerSecond { get; set; }
 94      public float ADSMovementSpeedMetersPerSecond { get; set; }
 95      
 96      // Commitment Unit (bullets per reaction window)
 97      public int BulletsPerCommitmentUnit { get; set; }
 98  
 99      public Weapon(string name)
100      {
101          Name = name;
102          // Set defaults
103          HeadshotMultiplier = 1.5f;
104          ADSMovementSpeedMultiplier = 0.6f;
105          BulletsPerCommitmentUnit = 3; // Default: reaction every 3 bullets
106          SuppressionFactor = 1.0f; // Default: AR-level suppression
107      }
108  
109      /// <summary>
110      /// Calculates time between shots in milliseconds.
111      /// </summary>
112      public float GetTimeBetweenShotsMs()
113      {
114          return 60000f / RoundsPerMinute;
115      }
116  
117      /// <summary>
118      /// Calculates damage at a given distance.
119      /// When one or more <see cref="DamageRange"/> entries are configured, uses them to determine
120      /// damage based on distance brackets; otherwise falls back to <see cref="BaseDamage"/>.
121      /// The <see cref="HeadshotMultiplier"/> is applied on top of the chosen base damage when
122      /// <paramref name="isHeadshot"/> is <c>true</c>.
123      /// </summary>
124      public float GetDamageAtDistance(float distance, bool isHeadshot = false)
125      {
126          float damage = BaseDamage;
127  
128          if (DamageRanges.Count > 0)
129          {
130              var range = DamageRanges.FirstOrDefault(r => distance >= r.MinMeters && distance < r.MaxMeters);
131              if (range == null)
132              {
133                  range = DamageRanges.Where(r => float.IsPositiveInfinity(r.MaxMeters))
134                      .OrderByDescending(r => r.MinMeters)
135                      .FirstOrDefault();
136              }
137  
138              damage = range?.Damage ?? BaseDamage;
139          }
140          
141          // Apply headshot multiplier
142          if (isHeadshot)
143          {
144              damage *= HeadshotMultiplier;
145          }
146          
147          return damage;
148      }
149  }
150  
151  public sealed record DamageRange(float MinMeters, float MaxMeters, float Damage)
152  {
153      public IReadOnlyDictionary<BodyPart, float>? BodyPartDamageOverrides { get; init; }
154  }
155  
156  public enum BodyPart
157  {
158      LowerLeg,
159      UpperLeg,
160      LowerArm,
161      UpperArm,
162      LowerTorso,
163      UpperTorso,
164      Neck,
165      Head,
166      Miss  // Indicates shot missed entirely (overshoot or undershoot)
167  }