CaptureAreaSystem.cs 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. using Content.Server.GameTicking.Rules.Components;
  2. using Content.Shared.NPC.Components;
  3. using Content.Shared.Physics;
  4. using Robust.Shared.Timing;
  5. using Content.Server.Chat.Systems;
  6. using Content.Server.RoundEnd;
  7. using Content.Shared.Mobs.Components;
  8. using Content.Shared.Mobs;
  9. namespace Content.Server.GameTicking.Rules;
  10. public sealed class CaptureAreaSystem : GameRuleSystem<CaptureAreaRuleComponent>
  11. {
  12. [Dependency] private readonly SharedTransformSystem _transform = default!;
  13. [Dependency] private readonly IEntityManager _entityManager = default!;
  14. [Dependency] private readonly EntityLookupSystem _lookup = default!;
  15. [Dependency] private readonly IGameTiming _timing = default!;
  16. [Dependency] private readonly ChatSystem _chat = default!;
  17. [Dependency] private readonly RoundEndSystem _roundEndSystem = default!;
  18. [Dependency] private readonly GameTicker _gameTicker = default!;
  19. public override void Initialize()
  20. {
  21. base.Initialize();
  22. }
  23. public override void Update(float frameTime)
  24. {
  25. base.Update(frameTime);
  26. var query = EntityQueryEnumerator<CaptureAreaComponent>();
  27. while (query.MoveNext(out var uid, out var area))
  28. {
  29. ProcessArea(uid, area, frameTime);
  30. }
  31. }
  32. /// <summary>
  33. /// Processes a capture area, determining faction control based on the presence of alive faction members, updating control status, managing capture timers, and dispatching global announcements for control changes, timed warnings, and victory.
  34. /// </summary>
  35. /// <param name="uid">The entity identifier of the capture area.</param>
  36. /// <param name="area">The capture area component to process.</param>
  37. /// <param name="frameTime">The elapsed time since the last update, in seconds.</param>
  38. private void ProcessArea(EntityUid uid, CaptureAreaComponent area, float frameTime)
  39. {
  40. var areaXform = _transform.GetMapCoordinates(uid);
  41. var factionCounts = new Dictionary<string, int>();
  42. // Initialize counts for all capturable factions to 0
  43. foreach (var faction in area.CapturableFactions)
  44. {
  45. factionCounts[faction] = 0;
  46. }
  47. // Find entities in range and count factions
  48. var entitiesInRange = _lookup.GetEntitiesInRange(areaXform, area.CaptureRadius, LookupFlags.Dynamic | LookupFlags.Sundries); // Include dynamic entities and items/mobs etc.
  49. foreach (var entity in entitiesInRange)
  50. {
  51. if (EntityManager.TryGetComponent<MobStateComponent>(entity, out var mobState))
  52. {
  53. //do not count dead and crit mobs
  54. if (mobState.CurrentState == MobState.Alive)
  55. // Check if the entity has a faction and if it's one we care about
  56. if (_entityManager.TryGetComponent<NpcFactionMemberComponent>(entity, out var factionMember))
  57. {
  58. foreach (var faction in factionMember.Factions)
  59. {
  60. if (area.CapturableFactions.Contains(faction))
  61. factionCounts[faction]++;
  62. }
  63. }
  64. }
  65. }
  66. // Determine the controlling faction
  67. var currentController = "";
  68. var maxCount = 0;
  69. foreach (var (faction, count) in factionCounts)
  70. {
  71. if (count > maxCount)
  72. {
  73. maxCount = count;
  74. currentController = faction;
  75. }
  76. else if (maxCount != 0 && count == maxCount)
  77. {
  78. currentController = ""; // Contested
  79. }
  80. }
  81. // Update component state
  82. if (maxCount > 0 && currentController != "")
  83. {
  84. area.Occupied = true;
  85. }
  86. if (currentController != area.Controller)
  87. {
  88. // Controller changed (or became contested/empty)
  89. if (currentController == "")
  90. {
  91. // Area became contested or empty
  92. if (area.ContestedTimer == 0f)
  93. {
  94. // Store the last controller when we first enter contested state
  95. area.LastController = area.Controller;
  96. }
  97. // Increment contested timer
  98. area.ContestedTimer += frameTime;
  99. // Only reset the capture timer if contested for long enough
  100. if (area.ContestedTimer >= area.ContestedResetTime)
  101. {
  102. // Reset capture progress after contested threshold is reached
  103. area.CaptureTimer = 0f;
  104. area.CaptureTimerAnnouncement1 = false;
  105. area.CaptureTimerAnnouncement2 = false;
  106. // Only announce loss of control once the timer has fully reset
  107. if (!string.IsNullOrEmpty(area.LastController))
  108. {
  109. _chat.DispatchGlobalAnnouncement($"{area.LastController} has lost control of {area.Name}!", "Objective", false, null, Color.Red);
  110. area.LastController = ""; // Clear last controller after announcement
  111. }
  112. }
  113. }
  114. else if (area.Controller == "")
  115. {
  116. // Area was contested/empty but now has a controller
  117. if (currentController == area.LastController && area.ContestedTimer < area.ContestedResetTime)
  118. {
  119. // The previous controller regained control before the reset threshold
  120. // Don't reset the timer or make announcements
  121. area.Controller = currentController;
  122. area.ContestedTimer = 0f;
  123. }
  124. else
  125. {
  126. // New controller or contested long enough to reset
  127. area.Controller = currentController;
  128. area.ContestedTimer = 0f;
  129. _chat.DispatchGlobalAnnouncement($"{currentController} has gained control of {area.Name}!", "Objective", false, null, Color.DodgerBlue);
  130. }
  131. }
  132. else
  133. {
  134. // Direct change from one faction to another
  135. if (area.ContestedTimer == 0f)
  136. {
  137. // Store the last controller when we first enter contested state
  138. area.LastController = area.Controller;
  139. }
  140. // Treat as contested first
  141. area.Controller = "";
  142. area.ContestedTimer += frameTime;
  143. // Only reset and announce if contested long enough
  144. if (area.ContestedTimer >= area.ContestedResetTime)
  145. {
  146. area.CaptureTimer = 0f;
  147. area.CaptureTimerAnnouncement1 = false;
  148. area.CaptureTimerAnnouncement2 = false;
  149. // Now update to the new controller
  150. area.Controller = currentController;
  151. // Announce the change
  152. _chat.DispatchGlobalAnnouncement($"{area.LastController} has lost control of {area.Name}!", "Objective", false, null, Color.Red);
  153. _chat.DispatchGlobalAnnouncement($"{currentController} has gained control of {area.Name}!", "Objective", false, null, Color.DodgerBlue);
  154. area.LastController = "";
  155. area.ContestedTimer = 0f;
  156. }
  157. }
  158. }
  159. else if (!string.IsNullOrEmpty(currentController))
  160. {
  161. // Controller remains the same, reset contested timer and increment capture timer
  162. area.ContestedTimer = 0f;
  163. area.CaptureTimer += frameTime;
  164. //announce when theres 2 and 1 minutes left.
  165. var timeleft = area.CaptureDuration - area.CaptureTimer;
  166. if (timeleft <= 120 && area.CaptureTimerAnnouncement2 == false)
  167. {
  168. _chat.DispatchGlobalAnnouncement($"Two minutes until {currentController} captures {area.Name}!", "Round", false, null, Color.Blue);
  169. area.CaptureTimerAnnouncement2 = true;
  170. }
  171. else if (timeleft < 60 && area.CaptureTimerAnnouncement1 == false)
  172. {
  173. _chat.DispatchGlobalAnnouncement($"One minute until {currentController} captures {area.Name}!", "Round", false, null, Color.Blue);
  174. area.CaptureTimerAnnouncement1 = true;
  175. }
  176. //Check for capture completion
  177. if (area.CaptureTimer >= area.CaptureDuration)
  178. {
  179. if (_gameTicker.RunLevel == GameRunLevel.InRound)
  180. {
  181. _chat.DispatchGlobalAnnouncement($"{currentController} has captured {area.Name} and is victorious!", "Round", false, null, Color.Green);
  182. _roundEndSystem.EndRound();
  183. }
  184. }
  185. }
  186. else
  187. {
  188. // Area is empty or contested, and wasn't previously controlled by a single faction
  189. // Increment contested timer
  190. area.ContestedTimer += frameTime;
  191. if (area.ContestedTimer >= area.ContestedResetTime)
  192. {
  193. // Reset capture progress after contested threshold is reached
  194. area.CaptureTimer = 0f;
  195. area.CaptureTimerAnnouncement1 = false;
  196. area.CaptureTimerAnnouncement2 = false;
  197. }
  198. }
  199. area.PreviousController = currentController;
  200. }
  201. }