/ GUNRPG.Core / VirtualPet / PetRules.cs
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  }