CaptureAreaSystem.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  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. // Attempt to get the rule component.
  27. // The standard way in GameRuleSystem<T> is: var ruleComp = RuleConfiguration;
  28. // If 'RuleConfiguration' is not recognized by the compiler in your environment,
  29. // you can query for the component directly as a workaround.
  30. CaptureAreaRuleComponent? ruleComp = null;
  31. var ruleQuery = EntityQueryEnumerator<CaptureAreaRuleComponent>();
  32. if (ruleQuery.MoveNext(out _, out var activeRuleComp)) // Assumes one active rule component
  33. {
  34. ruleComp = activeRuleComp;
  35. }
  36. if (ruleComp == null) // No active CaptureAreaRuleComponent found
  37. {
  38. return;
  39. }
  40. // Handle Asymmetric mode timer and defender victory
  41. if (ruleComp.Mode == "Asymmetric")
  42. {
  43. // If the round has already ended (e.g., by a capture in ProcessArea earlier this frame), do nothing.
  44. if (_gameTicker.RunLevel != GameRunLevel.InRound)
  45. return;
  46. ruleComp.AsymmetricGameTimeElapsed += frameTime;
  47. if (ruleComp.AsymmetricGameTimeElapsed >= ruleComp.Timer * 60f) // ruleComp.Timer is in minutes
  48. {
  49. var defenderDisplayName = Faction2String(ruleComp.DefenderFactionName);
  50. if (string.IsNullOrEmpty(ruleComp.DefenderFactionName) || string.IsNullOrEmpty(defenderDisplayName))
  51. {
  52. Logger.ErrorS("capturearea", $"Asymmetric mode: DefenderFactionName is not set or Faction2String returned empty for '{ruleComp.DefenderFactionName}'. Defaulting defender display name.");
  53. defenderDisplayName = "The Defenders"; // Fallback display name
  54. }
  55. _chat.DispatchGlobalAnnouncement(
  56. $"{defenderDisplayName} ha(s) successfully defended for {ruleComp.Timer:F0} minutes and win(s) the round!",
  57. "Round", false, null, Color.Green);
  58. _roundEndSystem.EndRound();
  59. return; // Round ended, no need to process areas further for capture victories
  60. }
  61. }
  62. // Process individual capture areas
  63. var query = EntityQueryEnumerator<CaptureAreaComponent>();
  64. while (query.MoveNext(out var uid, out var area))
  65. {
  66. // If the round ended due to asymmetric timer, stop processing areas.
  67. if (_gameTicker.RunLevel != GameRunLevel.InRound)
  68. break;
  69. ProcessArea(uid, area, frameTime, ruleComp);
  70. }
  71. }
  72. /// <summary>
  73. /// 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.
  74. /// </summary>
  75. private void ProcessArea(EntityUid uid, CaptureAreaComponent area, float frameTime, CaptureAreaRuleComponent ruleComp)
  76. {
  77. var areaXform = _transform.GetMapCoordinates(uid);
  78. var factionCounts = new Dictionary<string, int>();
  79. // Initialize counts for all capturable factions to 0
  80. foreach (var faction in area.CapturableFactions)
  81. {
  82. factionCounts[faction] = 0;
  83. }
  84. // Find entities in range and count factions
  85. var entitiesInRange = _lookup.GetEntitiesInRange(areaXform, area.CaptureRadius, LookupFlags.Dynamic | LookupFlags.Sundries); // Include dynamic entities and items/mobs etc.
  86. foreach (var entity in entitiesInRange)
  87. {
  88. if (EntityManager.TryGetComponent<MobStateComponent>(entity, out var mobState))
  89. {
  90. //do not count dead and crit mobs
  91. if (mobState.CurrentState == MobState.Alive)
  92. // Check if the entity has a faction and if it's one we care about
  93. if (_entityManager.TryGetComponent<NpcFactionMemberComponent>(entity, out var factionMember))
  94. {
  95. foreach (var faction in factionMember.Factions)
  96. {
  97. if (area.CapturableFactions.Contains(faction))
  98. factionCounts[faction]++;
  99. }
  100. }
  101. }
  102. }
  103. // Determine the controlling faction
  104. var currentController = "";
  105. var maxCount = 0;
  106. foreach (var (faction, count) in factionCounts)
  107. {
  108. if (count > maxCount)
  109. {
  110. maxCount = count;
  111. currentController = Faction2String(faction);
  112. }
  113. else if (maxCount != 0 && count == maxCount)
  114. {
  115. currentController = ""; // Contested
  116. }
  117. }
  118. // Update component state
  119. area.Occupied = maxCount > 0 && !string.IsNullOrEmpty(currentController);
  120. if (currentController != area.Controller)
  121. {
  122. // Controller changed (or became contested/empty)
  123. if (currentController == "")
  124. {
  125. // Area became contested or empty
  126. if (area.ContestedTimer == 0f)
  127. {
  128. // Store the last controller when we first enter contested state
  129. area.LastController = area.Controller;
  130. }
  131. // Increment contested timer
  132. area.ContestedTimer += frameTime;
  133. // Only reset the capture timer if contested for long enough
  134. if (area.ContestedTimer >= area.ContestedResetTime)
  135. {
  136. // Reset capture progress after contested threshold is reached
  137. area.CaptureTimer = 0f;
  138. area.CaptureTimerAnnouncement1 = false;
  139. area.CaptureTimerAnnouncement2 = false;
  140. // Only announce loss of control once the timer has fully reset
  141. if (!string.IsNullOrEmpty(area.LastController))
  142. {
  143. _chat.DispatchGlobalAnnouncement($"{area.LastController} has lost control of {area.Name}!", "Objective", false, null, Color.Red);
  144. area.LastController = ""; // Clear last controller after announcement
  145. }
  146. }
  147. }
  148. else if (area.Controller == "")
  149. {
  150. // Area was contested/empty but now has a controller
  151. if (currentController == area.LastController && area.ContestedTimer < area.ContestedResetTime)
  152. {
  153. // The previous controller regained control before the reset threshold
  154. // Don't reset the timer or make announcements
  155. area.Controller = currentController;
  156. area.ContestedTimer = 0f;
  157. }
  158. else
  159. {
  160. // New controller or contested long enough to reset
  161. area.Controller = currentController;
  162. area.ContestedTimer = 0f;
  163. _chat.DispatchGlobalAnnouncement($"{currentController} has gained control of {area.Name}!", "Objective", false, null, Color.DodgerBlue);
  164. }
  165. }
  166. else
  167. {
  168. // Direct change from one faction to another
  169. var oldController = area.Controller; // This is the display name of the old controller
  170. // Announce loss for the old controller
  171. // oldController is guaranteed to be non-empty here because:
  172. // 1. currentController != area.Controller (outer condition)
  173. // 2. currentController != "" (otherwise this branch wouldn't be hit, it'd be currentController == "")
  174. // 3. area.Controller != "" (otherwise this branch wouldn't be hit, it'd be area.Controller == "")
  175. _chat.DispatchGlobalAnnouncement($"{oldController} has lost control of {area.Name}!", "Objective", false, null, Color.Red);
  176. // Announce gain for the new controller
  177. _chat.DispatchGlobalAnnouncement($"{currentController} has gained control of {area.Name}!", "Objective", false, null, Color.DodgerBlue);
  178. // Update to the new controller
  179. area.Controller = currentController;
  180. // Reset capture progress for the new controller
  181. area.CaptureTimer = 0f;
  182. area.CaptureTimerAnnouncement1 = false;
  183. area.CaptureTimerAnnouncement2 = false;
  184. // Reset contested state as it's now firmly controlled by a new faction
  185. area.ContestedTimer = 0f;
  186. area.LastController = ""; // Previous "last controller" during a contested phase is no longer relevant
  187. }
  188. }
  189. else if (!string.IsNullOrEmpty(currentController))
  190. {
  191. // Controller remains the same, reset contested timer and increment capture timer
  192. area.ContestedTimer = 0f;
  193. area.CaptureTimer += frameTime;
  194. //announce when theres 2 and 1 minutes left.
  195. var timeleft = area.CaptureDuration - area.CaptureTimer;
  196. if (currentController != Faction2String(ruleComp.DefenderFactionName))
  197. {
  198. if (timeleft <= 120 && area.CaptureTimerAnnouncement2 == false)
  199. {
  200. _chat.DispatchGlobalAnnouncement($"Two minutes until {currentController} captures {area.Name}!", "Round", false, null, Color.Blue);
  201. area.CaptureTimerAnnouncement2 = true;
  202. }
  203. else if (timeleft < 60 && area.CaptureTimerAnnouncement1 == false)
  204. {
  205. _chat.DispatchGlobalAnnouncement($"One minute until {currentController} captures {area.Name}!", "Round", false, null, Color.Blue);
  206. area.CaptureTimerAnnouncement1 = true;
  207. }
  208. }
  209. //Check for capture completion
  210. if (area.CaptureTimer >= area.CaptureDuration)
  211. {
  212. if (_gameTicker.RunLevel == GameRunLevel.InRound)
  213. {
  214. bool canWinByCapture = true;
  215. // area.Controller is the display name of the faction that has held the point
  216. string winningControllerDisplay = area.Controller;
  217. if (ruleComp.Mode == "Asymmetric")
  218. {
  219. // In Asymmetric mode, only non-defenders (attackers) can win by capturing a point.
  220. // The defender wins by timeout.
  221. if (winningControllerDisplay == Faction2String(ruleComp.DefenderFactionName))
  222. {
  223. canWinByCapture = false;
  224. }
  225. }
  226. if (canWinByCapture && !string.IsNullOrEmpty(winningControllerDisplay))
  227. {
  228. _chat.DispatchGlobalAnnouncement($"{winningControllerDisplay} has captured {area.Name} and is victorious!", "Round", false, null, Color.Green);
  229. _roundEndSystem.EndRound();
  230. return; // Round ended, no further processing for this area needed.
  231. }
  232. }
  233. }
  234. }
  235. else
  236. {
  237. // Area is empty or contested, and wasn't previously controlled by a single faction
  238. // Increment contested timer
  239. area.ContestedTimer += frameTime;
  240. if (area.ContestedTimer >= area.ContestedResetTime)
  241. {
  242. // Reset capture progress after contested threshold is reached
  243. area.CaptureTimer = 0f;
  244. area.CaptureTimerAnnouncement1 = false;
  245. area.CaptureTimerAnnouncement2 = false;
  246. }
  247. }
  248. area.PreviousController = currentController;
  249. }
  250. private static string Faction2String(string faction)
  251. {
  252. switch (faction)
  253. {
  254. case "SovietCW":
  255. return "Soviet Union";
  256. case "Soviet":
  257. return "Soviet Union";
  258. default:
  259. return faction;
  260. }
  261. }
  262. }