OpponentDifficulty.cs
1 namespace GUNRPG.Core.VirtualPet; 2 3 /// <summary> 4 /// Static utility class for computing opponent difficulty based on level differences and proficiencies. 5 /// Provides deterministic difficulty scaling for mission calculations with no dependencies on PetState or combat logic. 6 /// </summary> 7 public static class OpponentDifficulty 8 { 9 // ======================================== 10 // Tuning Constants 11 // ======================================== 12 13 /// <summary> 14 /// XP required per level in the square-root progression curve. 15 /// Higher values make leveling slower. 16 /// </summary> 17 private const long XpPerLevel = 1000L; 18 19 /// <summary> 20 /// Base difficulty when opponents are evenly matched (same level, same proficiencies). 21 /// </summary> 22 private const float BaseDifficulty = 50f; 23 24 /// <summary> 25 /// Difficulty adjustment per level difference. 26 /// Positive when opponent is higher level, negative when lower. 27 /// </summary> 28 private const float DifficultyPerLevel = 10f; 29 30 /// <summary> 31 /// Maximum difficulty contribution from weapon proficiency delta (±15). 32 /// Applied when proficiency difference is ±100. 33 /// </summary> 34 private const float MaxWeaponProficiencyImpact = 15f; 35 36 /// <summary> 37 /// Maximum difficulty contribution from general proficiency delta (±10). 38 /// Applied when proficiency difference is ±100. 39 /// </summary> 40 private const float MaxGeneralProficiencyImpact = 10f; 41 42 /// <summary> 43 /// Minimum difficulty value (prevents difficulty from going too low). 44 /// </summary> 45 private const float MinDifficulty = 10f; 46 47 /// <summary> 48 /// Maximum difficulty value (prevents difficulty from going too high). 49 /// </summary> 50 private const float MaxDifficulty = 100f; 51 52 // ======================================== 53 // Public Methods 54 // ======================================== 55 56 /// <summary> 57 /// Computes the difficulty rating of an opponent based on level difference (simple version). 58 /// </summary> 59 /// <param name="opponentLevel">The level of the opponent.</param> 60 /// <param name="playerLevel">The level of the player/operator.</param> 61 /// <returns> 62 /// A difficulty rating between 10 and 100, where: 63 /// - 50 represents an evenly matched opponent (same level) 64 /// - Each level difference adjusts difficulty by 10 points 65 /// - Higher opponent levels increase difficulty 66 /// - Lower opponent levels decrease difficulty 67 /// - Result is clamped to the range [10, 100] 68 /// </returns> 69 /// <remarks> 70 /// This is a pure utility function with no dependencies on PetState or combat logic. 71 /// The difficulty value can be used as input to mission calculations. 72 /// 73 /// Examples: 74 /// - Player level 5 vs Opponent level 5: Difficulty = 50 (evenly matched) 75 /// - Player level 5 vs Opponent level 8: Difficulty = 80 (opponent 3 levels higher) 76 /// - Player level 8 vs Opponent level 5: Difficulty = 20 (opponent 3 levels lower) 77 /// - Player level 1 vs Opponent level 20: Difficulty = 100 (clamped at maximum) 78 /// </remarks> 79 public static float Compute(int opponentLevel, int playerLevel) 80 { 81 // Calculate level difference (positive when opponent is stronger) 82 int levelDelta = opponentLevel - playerLevel; 83 84 // Base difficulty is 50 (evenly matched) 85 // Each level of difference adjusts by 10 points 86 float difficulty = BaseDifficulty + (levelDelta * DifficultyPerLevel); 87 88 // Clamp result between 10 and 100 89 return Math.Clamp(difficulty, MinDifficulty, MaxDifficulty); 90 } 91 92 /// <summary> 93 /// Computes the difficulty rating of an opponent based on XP, weapon proficiency, and general proficiency. 94 /// </summary> 95 /// <param name="opponentXp">Experience points of the opponent.</param> 96 /// <param name="playerXp">Experience points of the player/operator.</param> 97 /// <param name="opponentWeaponProficiency">Opponent's weapon proficiency (0-100).</param> 98 /// <param name="opponentGeneralProficiency">Opponent's general combat proficiency (0-100).</param> 99 /// <param name="playerWeaponProficiency">Player's weapon proficiency (0-100).</param> 100 /// <param name="playerGeneralProficiency">Player's general combat proficiency (0-100).</param> 101 /// <returns> 102 /// A difficulty rating between 10 and 100, where: 103 /// - Base difficulty starts at 50 (evenly matched) 104 /// - Level difference (from XP) contributes ±10 per level 105 /// - Weapon proficiency difference contributes up to ±15 106 /// - General proficiency difference contributes up to ±10 107 /// - Result is clamped to the range [10, 100] 108 /// </returns> 109 /// <remarks> 110 /// This method provides more nuanced difficulty calculation by considering: 111 /// 1. Experience-based level differences (derived via square-root curve) 112 /// 2. Weapon-specific skill differences 113 /// 3. General combat capability differences 114 /// 115 /// Proficiency modifiers are scaled linearly and have smaller impact than level differences, 116 /// ensuring that experience remains the primary factor in difficulty assessment. 117 /// 118 /// Examples: 119 /// - Equal XP, equal proficiencies: Difficulty = 50 120 /// - Opponent 9 levels higher (81k vs 0 XP): Difficulty increases by 90 (clamped to 100) 121 /// - Opponent with +50 weapon prof, +50 general prof: Difficulty increases by ~12.5 122 /// </remarks> 123 public static float Compute( 124 long opponentXp, 125 long playerXp, 126 float opponentWeaponProficiency, 127 float opponentGeneralProficiency, 128 float playerWeaponProficiency, 129 float playerGeneralProficiency) 130 { 131 // Derive levels from XP using square-root curve 132 int opponentLevel = ComputeLevelFromXp(opponentXp); 133 int playerLevel = ComputeLevelFromXp(playerXp); 134 135 // Calculate level difference contribution 136 int levelDelta = opponentLevel - playerLevel; 137 float difficulty = BaseDifficulty + (levelDelta * DifficultyPerLevel); 138 139 // Calculate weapon proficiency contribution 140 // Delta in range [-100, +100], scaled to max impact of ±15 141 float weaponProfDelta = opponentWeaponProficiency - playerWeaponProficiency; 142 float weaponImpact = (weaponProfDelta / 100f) * MaxWeaponProficiencyImpact; 143 difficulty += weaponImpact; 144 145 // Calculate general proficiency contribution 146 // Delta in range [-100, +100], scaled to max impact of ±10 147 float generalProfDelta = opponentGeneralProficiency - playerGeneralProficiency; 148 float generalImpact = (generalProfDelta / 100f) * MaxGeneralProficiencyImpact; 149 difficulty += generalImpact; 150 151 // Clamp final result to valid range 152 return Math.Clamp(difficulty, MinDifficulty, MaxDifficulty); 153 } 154 155 /// <summary> 156 /// Computes the level from experience points using a square-root progression curve. 157 /// </summary> 158 /// <param name="xp">Experience points (must be non-negative).</param> 159 /// <returns> 160 /// The level derived from XP, where: 161 /// - Level 0 requires 0 XP 162 /// - Level 1 requires 1,000 XP 163 /// - Level 2 requires 4,000 XP 164 /// - Level 3 requires 9,000 XP 165 /// - Level increases with the square root of XP 166 /// - Result is always >= 0 167 /// </returns> 168 /// <remarks> 169 /// Uses the formula: Level = floor(sqrt(xp / XpPerLevel)) 170 /// 171 /// This creates a progression where: 172 /// - Early levels are relatively quick to achieve 173 /// - Later levels require increasingly more XP 174 /// - The curve is smooth and continuous 175 /// 176 /// Example progression with XpPerLevel = 1000: 177 /// - 0 XP → Level 0 178 /// - 1,000 XP → Level 1 179 /// - 4,000 XP → Level 2 180 /// - 9,000 XP → Level 3 181 /// - 16,000 XP → Level 4 182 /// - 25,000 XP → Level 5 183 /// </remarks> 184 public static int ComputeLevelFromXp(long xp) 185 { 186 // Handle negative XP gracefully (shouldn't happen, but be defensive) 187 if (xp < 0) 188 { 189 return 0; 190 } 191 192 // Apply square-root curve: Level = floor(sqrt(xp / XpPerLevel)) 193 double scaledXp = (double)xp / XpPerLevel; 194 int level = (int)Math.Floor(Math.Sqrt(scaledXp)); 195 196 // Ensure level is non-negative 197 return Math.Max(0, level); 198 } 199 }