1
0

ZombieRuleSystem.cs 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. using Content.Server.Antag;
  2. using Content.Server.Chat.Systems;
  3. using Content.Server.GameTicking.Rules.Components;
  4. using Content.Server.Popups;
  5. using Content.Server.Roles;
  6. using Content.Server.RoundEnd;
  7. using Content.Server.Station.Components;
  8. using Content.Server.Station.Systems;
  9. using Content.Server.Zombies;
  10. using Content.Shared.GameTicking.Components;
  11. using Content.Shared.Humanoid;
  12. using Content.Shared.Mind;
  13. using Content.Shared.Mobs;
  14. using Content.Shared.Mobs.Components;
  15. using Content.Shared.Mobs.Systems;
  16. using Content.Shared.Roles;
  17. using Content.Shared.Zombies;
  18. using Robust.Shared.Player;
  19. using Robust.Shared.Timing;
  20. using System.Globalization;
  21. namespace Content.Server.GameTicking.Rules;
  22. public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
  23. {
  24. [Dependency] private readonly AntagSelectionSystem _antag = default!;
  25. [Dependency] private readonly ChatSystem _chat = default!;
  26. [Dependency] private readonly SharedMindSystem _mindSystem = default!;
  27. [Dependency] private readonly MobStateSystem _mobState = default!;
  28. [Dependency] private readonly PopupSystem _popup = default!;
  29. [Dependency] private readonly SharedRoleSystem _roles = default!;
  30. [Dependency] private readonly RoundEndSystem _roundEnd = default!;
  31. [Dependency] private readonly StationSystem _station = default!;
  32. [Dependency] private readonly IGameTiming _timing = default!;
  33. [Dependency] private readonly ZombieSystem _zombie = default!;
  34. public override void Initialize()
  35. {
  36. base.Initialize();
  37. SubscribeLocalEvent<InitialInfectedRoleComponent, GetBriefingEvent>(OnGetBriefing);
  38. SubscribeLocalEvent<ZombieRoleComponent, GetBriefingEvent>(OnGetBriefing);
  39. SubscribeLocalEvent<IncurableZombieComponent, ZombifySelfActionEvent>(OnZombifySelf);
  40. }
  41. private void OnGetBriefing(Entity<InitialInfectedRoleComponent> role, ref GetBriefingEvent args)
  42. {
  43. if (!_roles.MindHasRole<ZombieRoleComponent>(args.Mind.Owner))
  44. args.Append(Loc.GetString("zombie-patientzero-role-greeting"));
  45. }
  46. private void OnGetBriefing(Entity<ZombieRoleComponent> role, ref GetBriefingEvent args)
  47. {
  48. args.Append(Loc.GetString("zombie-infection-greeting"));
  49. }
  50. protected override void AppendRoundEndText(EntityUid uid,
  51. ZombieRuleComponent component,
  52. GameRuleComponent gameRule,
  53. ref RoundEndTextAppendEvent args)
  54. {
  55. base.AppendRoundEndText(uid, component, gameRule, ref args);
  56. // This is just the general condition thing used for determining the win/lose text
  57. var fraction = GetInfectedFraction(true, true);
  58. if (fraction <= 0)
  59. args.AddLine(Loc.GetString("zombie-round-end-amount-none"));
  60. else if (fraction <= 0.25)
  61. args.AddLine(Loc.GetString("zombie-round-end-amount-low"));
  62. else if (fraction <= 0.5)
  63. args.AddLine(Loc.GetString("zombie-round-end-amount-medium", ("percent", Math.Round((fraction * 100), 2).ToString(CultureInfo.InvariantCulture))));
  64. else if (fraction < 1)
  65. args.AddLine(Loc.GetString("zombie-round-end-amount-high", ("percent", Math.Round((fraction * 100), 2).ToString(CultureInfo.InvariantCulture))));
  66. else
  67. args.AddLine(Loc.GetString("zombie-round-end-amount-all"));
  68. var antags = _antag.GetAntagIdentifiers(uid);
  69. args.AddLine(Loc.GetString("zombie-round-end-initial-count", ("initialCount", antags.Count)));
  70. foreach (var (_, data, entName) in antags)
  71. {
  72. args.AddLine(Loc.GetString("zombie-round-end-user-was-initial",
  73. ("name", entName),
  74. ("username", data.UserName)));
  75. }
  76. var healthy = GetHealthyHumans();
  77. // Gets a bunch of the living players and displays them if they're under a threshold.
  78. // InitialInfected is used for the threshold because it scales with the player count well.
  79. if (healthy.Count <= 0 || healthy.Count > 2 * antags.Count)
  80. return;
  81. args.AddLine("");
  82. args.AddLine(Loc.GetString("zombie-round-end-survivor-count", ("count", healthy.Count)));
  83. foreach (var survivor in healthy)
  84. {
  85. var meta = MetaData(survivor);
  86. var username = string.Empty;
  87. if (_mindSystem.TryGetMind(survivor, out _, out var mind) && mind.Session != null)
  88. {
  89. username = mind.Session.Name;
  90. }
  91. args.AddLine(Loc.GetString("zombie-round-end-user-was-survivor",
  92. ("name", meta.EntityName),
  93. ("username", username)));
  94. }
  95. }
  96. /// <summary>
  97. /// The big kahoona function for checking if the round is gonna end
  98. /// </summary>
  99. private void CheckRoundEnd(ZombieRuleComponent zombieRuleComponent)
  100. {
  101. var healthy = GetHealthyHumans();
  102. if (healthy.Count == 1) // Only one human left. spooky
  103. _popup.PopupEntity(Loc.GetString("zombie-alone"), healthy[0], healthy[0]);
  104. if (GetInfectedFraction(false) > zombieRuleComponent.ZombieShuttleCallPercentage && !_roundEnd.IsRoundEndRequested())
  105. {
  106. foreach (var station in _station.GetStations())
  107. {
  108. _chat.DispatchStationAnnouncement(station, Loc.GetString("zombie-shuttle-call"), colorOverride: Color.Crimson);
  109. }
  110. _roundEnd.RequestRoundEnd(null, false);
  111. }
  112. // we include dead for this count because we don't want to end the round
  113. // when everyone gets on the shuttle.
  114. if (GetInfectedFraction() >= 1) // Oops, all zombies
  115. _roundEnd.EndRound();
  116. }
  117. protected override void Started(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
  118. {
  119. base.Started(uid, component, gameRule, args);
  120. component.NextRoundEndCheck = _timing.CurTime + component.EndCheckDelay;
  121. }
  122. protected override void ActiveTick(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, float frameTime)
  123. {
  124. base.ActiveTick(uid, component, gameRule, frameTime);
  125. if (!component.NextRoundEndCheck.HasValue || component.NextRoundEndCheck > _timing.CurTime)
  126. return;
  127. CheckRoundEnd(component);
  128. component.NextRoundEndCheck = _timing.CurTime + component.EndCheckDelay;
  129. }
  130. private void OnZombifySelf(EntityUid uid, IncurableZombieComponent component, ZombifySelfActionEvent args)
  131. {
  132. _zombie.ZombifyEntity(uid);
  133. if (component.Action != null)
  134. Del(component.Action.Value);
  135. }
  136. /// <summary>
  137. /// Get the fraction of players that are infected, between 0 and 1
  138. /// </summary>
  139. /// <param name="includeOffStation">Include healthy players that are not on the station grid</param>
  140. /// <param name="includeDead">Should dead zombies be included in the count</param>
  141. /// <returns></returns>
  142. private float GetInfectedFraction(bool includeOffStation = true, bool includeDead = false)
  143. {
  144. var players = GetHealthyHumans(includeOffStation);
  145. var zombieCount = 0;
  146. var query = EntityQueryEnumerator<HumanoidAppearanceComponent, ZombieComponent, MobStateComponent>();
  147. while (query.MoveNext(out _, out _, out _, out var mob))
  148. {
  149. if (!includeDead && mob.CurrentState == MobState.Dead)
  150. continue;
  151. zombieCount++;
  152. }
  153. return zombieCount / (float) (players.Count + zombieCount);
  154. }
  155. /// <summary>
  156. /// Gets the list of humans who are alive, not zombies, and are on a station.
  157. /// Flying off via a shuttle disqualifies you.
  158. /// </summary>
  159. /// <returns></returns>
  160. private List<EntityUid> GetHealthyHumans(bool includeOffStation = true)
  161. {
  162. var healthy = new List<EntityUid>();
  163. var stationGrids = new HashSet<EntityUid>();
  164. if (!includeOffStation)
  165. {
  166. foreach (var station in _station.GetStationsSet())
  167. {
  168. if (TryComp<StationDataComponent>(station, out var data) && _station.GetLargestGrid(data) is { } grid)
  169. stationGrids.Add(grid);
  170. }
  171. }
  172. var players = AllEntityQuery<HumanoidAppearanceComponent, ActorComponent, MobStateComponent, TransformComponent>();
  173. var zombers = GetEntityQuery<ZombieComponent>();
  174. while (players.MoveNext(out var uid, out _, out _, out var mob, out var xform))
  175. {
  176. if (!_mobState.IsAlive(uid, mob))
  177. continue;
  178. if (zombers.HasComponent(uid))
  179. continue;
  180. if (!includeOffStation && !stationGrids.Contains(xform.GridUid ?? EntityUid.Invalid))
  181. continue;
  182. healthy.Add(uid);
  183. }
  184. return healthy;
  185. }
  186. }