PetRules.cs
1 namespace GUNRPG.Core.VirtualPet; 2 3 /// <summary> 4 /// Pure functional rules engine for applying state transitions to PetState. 5 /// Contains no mutable state and performs deterministic transformations. 6 /// </summary> 7 public static class PetRules 8 { 9 /// <summary> 10 /// Applies background decay and a PetInput action to produce a new PetState. 11 /// This is a pure function with no side effects or mutable state. 12 /// </summary> 13 /// <param name="state">The current pet state.</param> 14 /// <param name="input">The input action to apply.</param> 15 /// <param name="now">The current time for calculating elapsed time.</param> 16 /// <returns>A new PetState with applied changes.</returns> 17 public static PetState Apply(PetState state, PetInput input, DateTimeOffset now) 18 { 19 // Calculate time elapsed since last update 20 TimeSpan elapsed = now - state.LastUpdated; 21 22 // Apply background decay based on elapsed time 23 var decayedState = ApplyBackgroundDecay(state, elapsed); 24 25 // Apply the specific input action 26 var finalState = input switch 27 { 28 RestInput rest => ApplyRest(decayedState, rest), 29 EatInput eat => ApplyEat(decayedState, eat), 30 DrinkInput drink => ApplyDrink(decayedState, drink), 31 MissionInput mission => ApplyMission(decayedState, mission), 32 _ => decayedState 33 }; 34 35 // Update the timestamp and return 36 return finalState with { LastUpdated = now }; 37 } 38 39 /// <summary> 40 /// Applies background decay to all stats based on elapsed time. 41 /// Implements conditional coupling rules where certain stats affect others. 42 /// </summary> 43 private static PetState ApplyBackgroundDecay(PetState state, TimeSpan elapsed) 44 { 45 float hours = (float)elapsed.TotalHours; 46 47 // Base decay amounts (always applied) 48 float hungerIncrease = PetConstants.HungerIncreasePerHour * hours; 49 float hydrationDecrease = PetConstants.HydrationDecreasePerHour * hours; 50 51 // Fatigue increases at base rate, faster when stress is high 52 float fatigueIncrease = PetConstants.FatigueIncreasePerHour * hours; 53 if (state.Stress > PetConstants.HighStressThreshold) 54 { 55 fatigueIncrease += PetConstants.FatigueIncreaseHighStress * hours; 56 } 57 58 // Stress increases at base rate, faster when injury is high 59 float stressIncrease = PetConstants.StressIncreasePerHour * hours; 60 if (state.Injury > PetConstants.HighInjuryThreshold) 61 { 62 stressIncrease += PetConstants.StressIncreaseHighInjury * hours; 63 } 64 65 // Morale decreases slowly when stress is elevated 66 float moraleDecrease = 0f; 67 if (state.Stress > PetConstants.MoraleDecayStressThreshold) 68 { 69 moraleDecrease = PetConstants.MoraleDecayPerHour * hours; 70 } 71 72 // Health decay is conditional on critical conditions 73 float healthDecrease = 0f; 74 bool healthDecayActive = state.Hunger > PetConstants.CriticalHungerThreshold 75 || state.Hydration < PetConstants.CriticalHydrationThreshold 76 || state.Injury > PetConstants.CriticalInjuryThreshold; 77 78 if (healthDecayActive) 79 { 80 healthDecrease = PetConstants.HealthDecayPerHour * hours; 81 // Morale decreases faster while health is decaying 82 moraleDecrease += PetConstants.MoraleDecayDuringHealthDecay * hours; 83 } 84 85 // Calculate new stat values (unclamped) 86 float newHunger = state.Hunger + hungerIncrease; 87 float newHydration = state.Hydration - hydrationDecrease; 88 float newFatigue = state.Fatigue + fatigueIncrease; 89 float newStress = state.Stress + stressIncrease; 90 float newMorale = state.Morale - moraleDecrease; 91 float newHealth = state.Health - healthDecrease; 92 93 // Apply all changes and clamp once at the end 94 return state with 95 { 96 Hunger = Clamp(newHunger), 97 Hydration = Clamp(newHydration), 98 Fatigue = Clamp(newFatigue), 99 Stress = Clamp(newStress), 100 Morale = Clamp(newMorale), 101 Health = Clamp(newHealth) 102 }; 103 } 104 105 /// <summary> 106 /// Applies rest action to the pet state. 107 /// Rest reduces fatigue and stress, and recovers health. 108 /// Recovery rates are proportionally reduced based on adverse conditions. 109 /// </summary> 110 private static PetState ApplyRest(PetState state, RestInput input) 111 { 112 float hours = (float)input.Duration.TotalHours; 113 114 // Calculate base recovery amounts 115 float healthRecovery = PetConstants.HealthRecoveryPerHour * hours; 116 float fatigueRecovery = PetConstants.FatigueRecoveryPerHour * hours; 117 float stressRecovery = PetConstants.StressRecoveryPerHour * hours; 118 119 // Apply recovery reduction multipliers based on current state 120 121 // Health recovery is reduced when injured 122 if (state.Injury > PetConstants.InjuryRecoveryReductionThreshold) 123 { 124 float injuryFactor = (state.Injury - PetConstants.InjuryRecoveryReductionThreshold) 125 / (PetConstants.MaxStatValue - PetConstants.InjuryRecoveryReductionThreshold); 126 float healthMultiplier = 1f - (injuryFactor * (1f - PetConstants.MinRecoveryMultiplier)); 127 healthRecovery *= healthMultiplier; 128 } 129 130 // Stress recovery is reduced when hungry or dehydrated 131 float stressMultiplier = 1f; 132 if (state.Hunger > PetConstants.HungerStressRecoveryThreshold) 133 { 134 float hungerFactor = (state.Hunger - PetConstants.HungerStressRecoveryThreshold) 135 / (PetConstants.MaxStatValue - PetConstants.HungerStressRecoveryThreshold); 136 stressMultiplier = Math.Min(stressMultiplier, 1f - (hungerFactor * (1f - PetConstants.MinRecoveryMultiplier))); 137 } 138 if (state.Hydration < PetConstants.HydrationStressRecoveryThreshold) 139 { 140 float hydrationFactor = (PetConstants.HydrationStressRecoveryThreshold - state.Hydration) 141 / PetConstants.HydrationStressRecoveryThreshold; 142 stressMultiplier = Math.Min(stressMultiplier, 1f - (hydrationFactor * (1f - PetConstants.MinRecoveryMultiplier))); 143 } 144 stressRecovery *= stressMultiplier; 145 146 // Fatigue recovery is reduced when stressed 147 if (state.Stress > PetConstants.StressFatigueRecoveryThreshold) 148 { 149 float stressFactor = (state.Stress - PetConstants.StressFatigueRecoveryThreshold) 150 / (PetConstants.MaxStatValue - PetConstants.StressFatigueRecoveryThreshold); 151 float fatigueMultiplier = 1f - (stressFactor * (1f - PetConstants.MinRecoveryMultiplier)); 152 fatigueRecovery *= fatigueMultiplier; 153 } 154 155 // Apply recovery and clamp to valid ranges 156 return state with 157 { 158 Health = Clamp(state.Health + healthRecovery), 159 Fatigue = Clamp(state.Fatigue - fatigueRecovery), 160 Stress = Clamp(state.Stress - stressRecovery) 161 }; 162 } 163 164 /// <summary> 165 /// Applies eating action to the pet state. 166 /// Eating reduces hunger. 167 /// </summary> 168 private static PetState ApplyEat(PetState state, EatInput input) 169 { 170 // Reduce hunger by nutrition value (assuming lower hunger is better) 171 return state with 172 { 173 Hunger = Clamp(state.Hunger - input.Nutrition) 174 }; 175 } 176 177 /// <summary> 178 /// Applies drinking action to the pet state. 179 /// Drinking increases hydration. 180 /// </summary> 181 private static PetState ApplyDrink(PetState state, DrinkInput input) 182 { 183 // Increase hydration 184 return state with 185 { 186 Hydration = Clamp(state.Hydration + input.Hydration) 187 }; 188 } 189 190 /// <summary> 191 /// Applies mission action to the pet state. 192 /// Missions affect injury, stress, fatigue, and potentially morale based on mission parameters. 193 /// </summary> 194 private static PetState ApplyMission(PetState state, MissionInput input) 195 { 196 // Calculate injury from hits taken 197 // Base injury is reduced proportionally by current health (healthier operators take less lasting injury) 198 float baseInjury = input.HitsTaken * PetConstants.InjuryPerHit; 199 float healthFactor = state.Health / PetConstants.MaxStatValue; // 0 to 1, higher is better 200 float injuryReduction = healthFactor * 0.5f; // Up to 50% reduction at full health 201 float actualInjury = baseInjury * (1f - injuryReduction); 202 203 // Calculate stress from opponent difficulty 204 float baseStress = input.OpponentDifficulty * PetConstants.StressPerDifficultyPoint; 205 206 // Amplify stress gain when fatigue is high 207 float stressMultiplier = 1f; 208 if (state.Fatigue > PetConstants.HighFatigueStressAmplificationThreshold) 209 { 210 stressMultiplier = PetConstants.HighFatigueStressMultiplier; 211 } 212 float actualStress = baseStress * stressMultiplier; 213 214 // Fatigue increases modestly on every mission 215 float fatigueIncrease = PetConstants.MissionFatigueIncrease; 216 217 // Calculate new stat values (unclamped) 218 float newInjury = state.Injury + actualInjury; 219 float newStress = state.Stress + actualStress; 220 float newFatigue = state.Fatigue + fatigueIncrease; 221 222 // Check if morale should decrease (after stress is applied) 223 float newMorale = state.Morale; 224 if (newStress > PetConstants.PostMissionStressThreshold) 225 { 226 newMorale = state.Morale - PetConstants.PostMissionMoraleDecrease; 227 } 228 229 // Apply all changes and clamp at the end 230 return state with 231 { 232 Injury = Clamp(newInjury), 233 Stress = Clamp(newStress), 234 Fatigue = Clamp(newFatigue), 235 Morale = Clamp(newMorale) 236 }; 237 } 238 239 /// <summary> 240 /// Clamps a stat value to the valid range defined by PetConstants. 241 /// </summary> 242 private static float Clamp(float value) 243 { 244 return Math.Clamp(value, PetConstants.MinStatValue, PetConstants.MaxStatValue); 245 } 246 }