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 }