using Content.Server.Antag; using Content.Server.Chat.Systems; using Content.Server.GameTicking.Rules.Components; using Content.Server.Popups; using Content.Server.Roles; using Content.Server.RoundEnd; using Content.Server.Station.Components; using Content.Server.Station.Systems; using Content.Server.Zombies; using Content.Shared.GameTicking.Components; using Content.Shared.Humanoid; using Content.Shared.Mind; using Content.Shared.Mobs; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; using Content.Shared.Roles; using Content.Shared.Zombies; using Robust.Shared.Player; using Robust.Shared.Timing; using System.Globalization; namespace Content.Server.GameTicking.Rules; public sealed class ZombieRuleSystem : GameRuleSystem { [Dependency] private readonly AntagSelectionSystem _antag = default!; [Dependency] private readonly ChatSystem _chat = default!; [Dependency] private readonly SharedMindSystem _mindSystem = default!; [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly SharedRoleSystem _roles = default!; [Dependency] private readonly RoundEndSystem _roundEnd = default!; [Dependency] private readonly StationSystem _station = default!; [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly ZombieSystem _zombie = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnGetBriefing); SubscribeLocalEvent(OnGetBriefing); SubscribeLocalEvent(OnZombifySelf); } private void OnGetBriefing(Entity role, ref GetBriefingEvent args) { if (!_roles.MindHasRole(args.Mind.Owner)) args.Append(Loc.GetString("zombie-patientzero-role-greeting")); } private void OnGetBriefing(Entity role, ref GetBriefingEvent args) { args.Append(Loc.GetString("zombie-infection-greeting")); } protected override void AppendRoundEndText(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, ref RoundEndTextAppendEvent args) { base.AppendRoundEndText(uid, component, gameRule, ref args); // This is just the general condition thing used for determining the win/lose text var fraction = GetInfectedFraction(true, true); if (fraction <= 0) args.AddLine(Loc.GetString("zombie-round-end-amount-none")); else if (fraction <= 0.25) args.AddLine(Loc.GetString("zombie-round-end-amount-low")); else if (fraction <= 0.5) args.AddLine(Loc.GetString("zombie-round-end-amount-medium", ("percent", Math.Round((fraction * 100), 2).ToString(CultureInfo.InvariantCulture)))); else if (fraction < 1) args.AddLine(Loc.GetString("zombie-round-end-amount-high", ("percent", Math.Round((fraction * 100), 2).ToString(CultureInfo.InvariantCulture)))); else args.AddLine(Loc.GetString("zombie-round-end-amount-all")); var antags = _antag.GetAntagIdentifiers(uid); args.AddLine(Loc.GetString("zombie-round-end-initial-count", ("initialCount", antags.Count))); foreach (var (_, data, entName) in antags) { args.AddLine(Loc.GetString("zombie-round-end-user-was-initial", ("name", entName), ("username", data.UserName))); } var healthy = GetHealthyHumans(); // Gets a bunch of the living players and displays them if they're under a threshold. // InitialInfected is used for the threshold because it scales with the player count well. if (healthy.Count <= 0 || healthy.Count > 2 * antags.Count) return; args.AddLine(""); args.AddLine(Loc.GetString("zombie-round-end-survivor-count", ("count", healthy.Count))); foreach (var survivor in healthy) { var meta = MetaData(survivor); var username = string.Empty; if (_mindSystem.TryGetMind(survivor, out _, out var mind) && mind.Session != null) { username = mind.Session.Name; } args.AddLine(Loc.GetString("zombie-round-end-user-was-survivor", ("name", meta.EntityName), ("username", username))); } } /// /// The big kahoona function for checking if the round is gonna end /// private void CheckRoundEnd(ZombieRuleComponent zombieRuleComponent) { var healthy = GetHealthyHumans(); if (healthy.Count == 1) // Only one human left. spooky _popup.PopupEntity(Loc.GetString("zombie-alone"), healthy[0], healthy[0]); if (GetInfectedFraction(false) > zombieRuleComponent.ZombieShuttleCallPercentage && !_roundEnd.IsRoundEndRequested()) { foreach (var station in _station.GetStations()) { _chat.DispatchStationAnnouncement(station, Loc.GetString("zombie-shuttle-call"), colorOverride: Color.Crimson); } _roundEnd.RequestRoundEnd(null, false); } // we include dead for this count because we don't want to end the round // when everyone gets on the shuttle. if (GetInfectedFraction() >= 1) // Oops, all zombies _roundEnd.EndRound(); } protected override void Started(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) { base.Started(uid, component, gameRule, args); component.NextRoundEndCheck = _timing.CurTime + component.EndCheckDelay; } protected override void ActiveTick(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, float frameTime) { base.ActiveTick(uid, component, gameRule, frameTime); if (!component.NextRoundEndCheck.HasValue || component.NextRoundEndCheck > _timing.CurTime) return; CheckRoundEnd(component); component.NextRoundEndCheck = _timing.CurTime + component.EndCheckDelay; } private void OnZombifySelf(EntityUid uid, IncurableZombieComponent component, ZombifySelfActionEvent args) { _zombie.ZombifyEntity(uid); if (component.Action != null) Del(component.Action.Value); } /// /// Get the fraction of players that are infected, between 0 and 1 /// /// Include healthy players that are not on the station grid /// Should dead zombies be included in the count /// private float GetInfectedFraction(bool includeOffStation = true, bool includeDead = false) { var players = GetHealthyHumans(includeOffStation); var zombieCount = 0; var query = EntityQueryEnumerator(); while (query.MoveNext(out _, out _, out _, out var mob)) { if (!includeDead && mob.CurrentState == MobState.Dead) continue; zombieCount++; } return zombieCount / (float) (players.Count + zombieCount); } /// /// Gets the list of humans who are alive, not zombies, and are on a station. /// Flying off via a shuttle disqualifies you. /// /// private List GetHealthyHumans(bool includeOffStation = true) { var healthy = new List(); var stationGrids = new HashSet(); if (!includeOffStation) { foreach (var station in _station.GetStationsSet()) { if (TryComp(station, out var data) && _station.GetLargestGrid(data) is { } grid) stationGrids.Add(grid); } } var players = AllEntityQuery(); var zombers = GetEntityQuery(); while (players.MoveNext(out var uid, out _, out _, out var mob, out var xform)) { if (!_mobState.IsAlive(uid, mob)) continue; if (zombers.HasComponent(uid)) continue; if (!includeOffStation && !stationGrids.Contains(xform.GridUid ?? EntityUid.Invalid)) continue; healthy.Add(uid); } return healthy; } }