RestSystem.cs
1 using GUNRPG.Core.Operators; 2 3 namespace GUNRPG.Core.VirtualPet; 4 5 /// <summary> 6 /// Manages the virtual pet loop - operator rest, recovery, and readiness. 7 /// </summary> 8 public class RestSystem 9 { 10 /// <summary> 11 /// Amount of fatigue gained per combat encounter. 12 /// </summary> 13 public float FatiguePerCombat { get; set; } = 20f; 14 15 /// <summary> 16 /// Fatigue recovery rate per hour of rest. 17 /// </summary> 18 public float FatigueRecoveryPerHour { get; set; } = 30f; 19 20 /// <summary> 21 /// Maximum fatigue before operator cannot deploy. 22 /// </summary> 23 public float MaxDeployableFatigue { get; set; } = 80f; 24 25 /// <summary> 26 /// Checks if an operator is ready for combat. 27 /// </summary> 28 public bool IsReadyForCombat(Operator op) 29 { 30 return op.Fatigue < MaxDeployableFatigue && op.IsAlive; 31 } 32 33 /// <summary> 34 /// Applies fatigue after a combat encounter. 35 /// </summary> 36 public void ApplyPostCombatFatigue(Operator op) 37 { 38 op.Fatigue = Math.Min(op.MaxFatigue, op.Fatigue + FatiguePerCombat); 39 } 40 41 /// <summary> 42 /// Recovers fatigue during rest. 43 /// </summary> 44 /// <param name="op">Operator resting</param> 45 /// <param name="hoursRested">Duration of rest in hours</param> 46 public void Rest(Operator op, float hoursRested) 47 { 48 float fatigueRecovered = FatigueRecoveryPerHour * hoursRested; 49 op.Fatigue = Math.Max(0, op.Fatigue - fatigueRecovered); 50 51 // Fully restore health during rest 52 op.Health = op.MaxHealth; 53 op.Stamina = op.MaxStamina; 54 op.LastDamageTimeMs = null; 55 } 56 57 /// <summary> 58 /// Gets the minimum rest time required for the operator to be combat-ready. 59 /// </summary> 60 public float GetMinimumRestHours(Operator op) 61 { 62 if (IsReadyForCombat(op)) 63 return 0f; 64 65 float excessFatigue = op.Fatigue - MaxDeployableFatigue; 66 return excessFatigue / FatigueRecoveryPerHour; 67 } 68 69 /// <summary> 70 /// Gets a status summary for the operator. 71 /// </summary> 72 public string GetOperatorStatus(Operator op) 73 { 74 if (!op.IsAlive) 75 return "❌ Deceased"; 76 77 if (IsReadyForCombat(op)) 78 return "✓ Ready for combat"; 79 80 float minRestHours = GetMinimumRestHours(op); 81 return $"⚠ Too fatigued (needs {minRestHours:F1}h rest)"; 82 } 83 } 84 85 /// <summary> 86 /// Manages operator schedule and availability. 87 /// </summary> 88 public class OperatorManager 89 { 90 private readonly RestSystem _restSystem; 91 private readonly Dictionary<Guid, DateTime> _restStartTimes; 92 93 public OperatorManager() 94 { 95 _restSystem = new RestSystem(); 96 _restStartTimes = new Dictionary<Guid, DateTime>(); 97 } 98 99 /// <summary> 100 /// Sends an operator to rest. 101 /// </summary> 102 public void SendToRest(Operator op) 103 { 104 _restStartTimes[op.Id] = DateTime.UtcNow; 105 } 106 107 /// <summary> 108 /// Checks if an operator is currently resting. 109 /// </summary> 110 public bool IsResting(Operator op) 111 { 112 return _restStartTimes.ContainsKey(op.Id); 113 } 114 115 /// <summary> 116 /// Gets the duration an operator has been resting. 117 /// </summary> 118 public TimeSpan GetRestDuration(Operator op) 119 { 120 if (!_restStartTimes.TryGetValue(op.Id, out var startTime)) 121 return TimeSpan.Zero; 122 123 return DateTime.UtcNow - startTime; 124 } 125 126 /// <summary> 127 /// Wakes an operator from rest, applying fatigue recovery. 128 /// </summary> 129 public void WakeFromRest(Operator op) 130 { 131 if (!_restStartTimes.TryGetValue(op.Id, out var startTime)) 132 return; 133 134 TimeSpan restDuration = DateTime.UtcNow - startTime; 135 float hoursRested = (float)restDuration.TotalHours; 136 137 _restSystem.Rest(op, hoursRested); 138 _restStartTimes.Remove(op.Id); 139 } 140 141 /// <summary> 142 /// Prepares an operator for combat deployment. 143 /// Fully restores health and stamina. 144 /// </summary> 145 public bool PrepareForCombat(Operator op) 146 { 147 if (!_restSystem.IsReadyForCombat(op)) 148 return false; 149 150 // Reset combat-scoped state 151 op.Health = op.MaxHealth; 152 op.Stamina = op.MaxStamina; 153 op.LastDamageTimeMs = null; 154 op.MovementState = MovementState.Idle; 155 op.AimState = AimState.Hip; 156 op.WeaponState = WeaponState.Ready; 157 op.CurrentRecoilX = 0; 158 op.CurrentRecoilY = 0; 159 op.RecoilRecoveryStartMs = null; 160 op.BulletsFiredSinceLastReaction = 0; 161 op.MetersMovedSinceLastReaction = 0; 162 163 if (op.EquippedWeapon != null) 164 { 165 op.CurrentAmmo = op.EquippedWeapon.MagazineSize; 166 } 167 168 return true; 169 } 170 171 /// <summary> 172 /// Handles post-combat cleanup. 173 /// </summary> 174 public void CompleteCombat(Operator op, bool survived) 175 { 176 if (survived) 177 { 178 _restSystem.ApplyPostCombatFatigue(op); 179 } 180 } 181 182 /// <summary> 183 /// Gets operator readiness status. 184 /// </summary> 185 public string GetStatus(Operator op) 186 { 187 if (IsResting(op)) 188 { 189 var duration = GetRestDuration(op); 190 return $"💤 Resting ({duration.Hours}h {duration.Minutes}m)"; 191 } 192 193 return _restSystem.GetOperatorStatus(op); 194 } 195 196 /// <summary> 197 /// Gets the rest system for configuration. 198 /// </summary> 199 public RestSystem RestSystem => _restSystem; 200 }