diff --git a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs index b9de9e8aa1..ce0e9ecc15 100644 --- a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs @@ -1,7 +1,6 @@ using System.Linq; using Content.Server.CharacterAppearance.Components; using Content.Server.Chat.Managers; -using Content.Server.GameTicking.Rules.Components; using Content.Server.GameTicking.Rules.Configurations; using Content.Server.Ghost.Roles.Components; using Content.Server.Ghost.Roles.Events; @@ -24,10 +23,16 @@ using Robust.Shared.Utility; using Content.Server.Traitor; using Content.Shared.MobState.Components; using System.Data; +using Content.Server.GameTicking.Rules.Components; +using Content.Server.Station.Components; +using Content.Shared.Chat; +using Content.Shared.Nuke; +using Robust.Server.GameObjects; using Content.Server.NPC.Components; using Content.Server.NPC.Systems; using Content.Server.Traitor.Uplink; using Robust.Shared.Audio; +using Robust.Shared.Configuration; using Robust.Shared.Player; namespace Content.Server.GameTicking.Rules; @@ -36,19 +41,67 @@ public sealed class NukeopsRuleSystem : GameRuleSystem { [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly IChatManager _chatManager = default!; [Dependency] private readonly IMapLoader _mapLoader = default!; [Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly StationSpawningSystem _stationSpawningSystem = default!; + [Dependency] private readonly StationSystem _stationSystem = default!; + [Dependency] private readonly ShuttleSystem _shuttleSystem = default!; [Dependency] private readonly RoundEndSystem _roundEndSystem = default!; [Dependency] private readonly IPlayerManager _playerSystem = default!; [Dependency] private readonly SharedAudioSystem _audioSystem = default!; - private bool _opsWon; + + private enum WinType + { + /// + /// Operative major win. This means they nuked the station. + /// + OpsMajor, + /// + /// Minor win. All nukies were alive at the end of the round. + /// Alternatively, some nukies were alive, but the disk was left behind. + /// + OpsMinor, + /// + /// Neutral win. The nuke exploded, but on the wrong station. + /// + Neutral, + /// + /// Crew minor win. The nuclear authentication disk escaped on the shuttle, + /// but some nukies were alive. + /// + CrewMinor, + /// + /// Crew major win. This means they either killed all nukies, + /// or the bomb exploded too far away from the station, or on the nukie moon. + /// + CrewMajor + } + + private enum WinCondition + { + NukeExplodedOnCorrectStation, + NukeExplodedOnNukieOutpost, + NukeExplodedOnIncorrectLocation, + NukeActiveInStation, + NukeActiveAtCentCom, + NukeDiskOnCentCom, + NukeDiskNotOnCentCom, + NukiesAbandoned, + AllNukiesDead, + SomeNukiesAlive, + AllNukiesAlive + } + + private WinType _winType = WinType.Neutral; + private List _winConditions = new (); private MapId? _nukiePlanet; private EntityUid? _nukieOutpost; private EntityUid? _nukieShuttle; + private EntityUid? _targetStation; public override string Prototype => "Nukeops"; @@ -84,6 +137,8 @@ public sealed class NukeopsRuleSystem : GameRuleSystem SubscribeLocalEvent(OnMobStateChanged); SubscribeLocalEvent(OnRoundEndText); SubscribeLocalEvent(OnNukeExploded); + SubscribeLocalEvent(OnRunLevelChanged); + SubscribeLocalEvent(OnNukeDisarm); SubscribeLocalEvent(OnPlayersGhostSpawning); SubscribeLocalEvent(OnMindAdded); SubscribeLocalEvent(OnComponentInit); @@ -111,16 +166,189 @@ public sealed class NukeopsRuleSystem : GameRuleSystem if (!RuleAdded) return; - _opsWon = true; + if (ev.OwningStation != null) + { + if (ev.OwningStation == _nukieOutpost) + { + _winType = WinType.CrewMajor; + _winConditions.Add(WinCondition.NukeExplodedOnNukieOutpost); + } + else + { + if (TryComp(_targetStation, out StationDataComponent? data)) + { + foreach (var grid in data.Grids) + { + if (grid != ev.OwningStation) + { + continue; + } + + _winType = WinType.OpsMajor; + _winConditions.Add(WinCondition.NukeExplodedOnCorrectStation); + return; + } + } + + _winConditions.Add(WinCondition.NukeExplodedOnIncorrectLocation); + } + } + else + { + _winConditions.Add(WinCondition.NukeExplodedOnIncorrectLocation); + } + _roundEndSystem.EndRound(); } + private void OnRunLevelChanged(GameRunLevelChangedEvent ev) + { + switch (ev.New) + { + case GameRunLevel.InRound: + OnRoundStart(); + break; + case GameRunLevel.PostRound: + OnRoundEnd(); + break; + } + } + + private void OnRoundStart() + { + // TODO: This needs to try and target a Nanotrasen station. At the very least, + // we can only currently guarantee that NT stations are the only station to + // exist in the base game. + + _targetStation = _stationSystem.Stations.FirstOrNull(); + + if (_targetStation == null) + { + return; + } + + foreach (var nukie in EntityQuery()) + { + if (!TryComp(nukie.Owner, out var actor)) + { + continue; + } + + _chatManager.DispatchServerMessage(actor.PlayerSession, Loc.GetString("nukeops-welcome", ("station", _targetStation.Value))); + } + } + + private void OnRoundEnd() + { + // If the win condition was set to operative/crew major win, ignore. + if (_winType == WinType.OpsMajor || _winType == WinType.CrewMajor) + { + return; + } + + foreach (var (nuke, nukeTransform) in EntityManager.EntityQuery(true)) + { + if (nuke.Status != NukeStatus.ARMED) + { + continue; + } + + // UH OH + if (nukeTransform.MapID == _shuttleSystem.CentComMap) + { + _winType = WinType.OpsMajor; + _winConditions.Add(WinCondition.NukeActiveAtCentCom); + return; + } + + if (nukeTransform.GridUid == null || _targetStation == null) + { + continue; + } + + if (!TryComp(_targetStation.Value, out StationDataComponent? data)) + { + continue; + } + + foreach (var grid in data.Grids) + { + if (grid != nukeTransform.GridUid) + { + continue; + } + + _winType = WinType.OpsMajor; + _winConditions.Add(WinCondition.NukeActiveInStation); + return; + } + } + + var allAlive = true; + foreach (var (_, state) in EntityQuery()) + { + if (state.CurrentState is DamageState.Alive) + { + continue; + } + + allAlive = false; + break; + } + // If all nuke ops were alive at the end of the round, + // the nuke ops win. This is to prevent people from + // running away the moment nuke ops appear. + if (allAlive) + { + _winType = WinType.OpsMinor; + _winConditions.Add(WinCondition.AllNukiesAlive); + return; + } + + _winConditions.Add(WinCondition.SomeNukiesAlive); + + var diskAtCentCom = false; + foreach (var (comp, transform) in EntityManager.EntityQuery()) + { + var diskMapId = transform.MapID; + diskAtCentCom = _shuttleSystem.CentComMap == diskMapId; + + // TODO: The target station should be stored, and the nuke disk should store its original station. + // This is fine for now, because we can assume a single station in base SS14. + break; + } + + // If the disk is currently at Central Command, the crew wins - just slightly. + // This also implies that some nuclear operatives have died. + if (diskAtCentCom) + { + _winType = WinType.CrewMinor; + _winConditions.Add(WinCondition.NukeDiskOnCentCom); + } + // Otherwise, the nuke ops win. + else + { + _winType = WinType.OpsMinor; + _winConditions.Add(WinCondition.NukeDiskNotOnCentCom); + } + } + private void OnRoundEndText(RoundEndTextAppendEvent ev) { if (!RuleAdded) return; - ev.AddLine(_opsWon ? Loc.GetString("nukeops-ops-won") : Loc.GetString("nukeops-crew-won")); + var winText = Loc.GetString($"nukeops-{_winType.ToString().ToLower()}"); + + ev.AddLine(winText); + + foreach (var cond in _winConditions) + { + var text = Loc.GetString($"nukeops-cond-{cond.ToString().ToLower()}"); + + ev.AddLine(text); + } + ev.AddLine(Loc.GetString("nukeops-list-start")); foreach (var nukeop in _operativePlayers) { @@ -133,29 +361,67 @@ public sealed class NukeopsRuleSystem : GameRuleSystem if (!RuleAdded) return; + // If there are any nuclear bombs that are active, immediately return. We're not over yet. + foreach (var nuke in EntityQuery()) + { + if (nuke.Status == NukeStatus.ARMED) + { + return; + } + } + MapId? shuttleMapId = EntityManager.EntityExists(_nukieShuttle) ? Transform(_nukieShuttle!.Value).MapID : null; - // Check if there are nuke operatives still alive on the same map as the shuttle. + MapId? targetStationMap = null; + if (_targetStation != null && TryComp(_targetStation, out StationDataComponent? data)) + { + var grid = data.Grids.FirstOrNull(); + targetStationMap = grid != null + ? Transform(grid.Value).MapID + : null; + } + + // Check if there are nuke operatives still alive on the same map as the shuttle, + // or on the same map as the station. // If there are, the round can continue. var operatives = EntityQuery(true); var operativesAlive = operatives - .Where(ent => ent.Item3.MapID == shuttleMapId) + .Where(ent => + ent.Item3.MapID == shuttleMapId + || ent.Item3.MapID == targetStationMap) .Any(ent => ent.Item2.CurrentState == DamageState.Alive && ent.Item1.Running); if (operativesAlive) - return; // There are living operatives than can access the shuttle. + return; // There are living operatives than can access the shuttle, or are still on the station's map. + + _winType = WinType.CrewMajor; // Check that there are spawns available and that they can access the shuttle. var spawnsAvailable = EntityQuery(true).Any(); if (spawnsAvailable && shuttleMapId == _nukiePlanet) return; // Ghost spawns can still access the shuttle. Continue the round. - // The shuttle is inaccessible to both living nuke operatives and yet to spawn nuke operatives. + // The shuttle is inaccessible to both living nuke operatives and yet to spawn nuke operatives, + // and there are no nuclear operatives on the target station's map. + if (spawnsAvailable) + { + _winConditions.Add(WinCondition.NukiesAbandoned); + } + else + { + _winConditions.Add(WinCondition.AllNukiesDead); + } + _roundEndSystem.EndRound(); } + private void OnNukeDisarm(NukeDisarmSuccessEvent ev) + { + CheckRoundShouldEnd(); + } + private void OnMobStateChanged(EntityUid uid, NukeOperativeComponent component, MobStateChangedEvent ev) { if(ev.CurrentMobState == DamageState.Dead) @@ -299,6 +565,9 @@ public sealed class NukeopsRuleSystem : GameRuleSystem if (_nukeopsRuleConfig.GreetSound != null) _audioSystem.PlayGlobal(_nukeopsRuleConfig.GreetSound, Filter.Empty().AddPlayer(playerSession), AudioParams.Default); + + if (_targetStation != null && !string.IsNullOrEmpty(Name(_targetStation.Value))) + _chatManager.DispatchServerMessage(playerSession, Loc.GetString("nukeops-welcome", ("station", _targetStation.Value))); } private bool SpawnMap() @@ -490,10 +759,12 @@ public sealed class NukeopsRuleSystem : GameRuleSystem private void OnStartAttempt(RoundStartAttemptEvent ev) { - if (!RuleAdded) + if (!RuleAdded || Configuration is not NukeopsRuleConfiguration nukeOpsConfig) return; - var minPlayers = _nukeopsRuleConfig.MinPlayers; + _nukeopsRuleConfig = nukeOpsConfig; + + var minPlayers = nukeOpsConfig.MinPlayers; if (!ev.Forced && ev.Players.Length < minPlayers) { _chatManager.DispatchServerAnnouncement(Loc.GetString("nukeops-not-enough-ready-players", ("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers))); @@ -510,7 +781,8 @@ public sealed class NukeopsRuleSystem : GameRuleSystem public override void Started() { - _opsWon = false; + _winType = WinType.Neutral; + _winConditions.Clear(); _nukieOutpost = null; _nukiePlanet = null; @@ -519,9 +791,6 @@ public sealed class NukeopsRuleSystem : GameRuleSystem _operativeMindPendingData.Clear(); _operativePlayers.Clear(); - if (Configuration is not NukeopsRuleConfiguration) - return; - // TODO: Loot table or something foreach (var proto in new[] { diff --git a/Content.Server/Nuke/NukeSystem.cs b/Content.Server/Nuke/NukeSystem.cs index df0b3d1b7a..df45d49edb 100644 --- a/Content.Server/Nuke/NukeSystem.cs +++ b/Content.Server/Nuke/NukeSystem.cs @@ -519,7 +519,10 @@ namespace Content.Server.Nuke component.IntensitySlope, component.MaxIntensity); - RaiseLocalEvent(new NukeExplodedEvent()); + RaiseLocalEvent(new NukeExplodedEvent() + { + OwningStation = transform.GridUid, + }); _soundSystem.StopStationEventMusic(component.Owner, StationEventMusicType.Nuke); EntityManager.DeleteEntity(uid); @@ -546,6 +549,7 @@ namespace Content.Server.Nuke { TargetCancelledEvent = new NukeDisarmCancelledEvent(), TargetFinishedEvent = new NukeDisarmSuccessEvent(), + BroadcastFinishedEvent = new NukeDisarmSuccessEvent(), BreakOnDamage = true, BreakOnStun = true, BreakOnTargetMove = true, @@ -570,7 +574,10 @@ namespace Content.Server.Nuke } } - public sealed class NukeExplodedEvent : EntityEventArgs {} + public sealed class NukeExplodedEvent : EntityEventArgs + { + public EntityUid? OwningStation; + } /// /// Raised directed on the nuke when its disarm doafter is successful. diff --git a/Content.Server/Shuttles/Systems/ShuttleSystem.EmergencyConsole.cs b/Content.Server/Shuttles/Systems/ShuttleSystem.EmergencyConsole.cs index 66825ed090..a1fe65824c 100644 --- a/Content.Server/Shuttles/Systems/ShuttleSystem.EmergencyConsole.cs +++ b/Content.Server/Shuttles/Systems/ShuttleSystem.EmergencyConsole.cs @@ -136,24 +136,24 @@ public sealed partial class ShuttleSystem { _launchedShuttles = true; - if (_centComMap != null) + if (CentComMap != null) { foreach (var comp in EntityQuery(true)) { if (!TryComp(comp.EmergencyShuttle, out var shuttle)) continue; - if (Deleted(_centCom)) + if (Deleted(CentCom)) { // TODO: Need to get non-overlapping positions. FTLTravel(shuttle, new EntityCoordinates( - _mapManager.GetMapEntityId(_centComMap.Value), + _mapManager.GetMapEntityId(CentComMap.Value), Vector2.One * 1000f), _consoleAccumulator, TransitTime); } else { FTLTravel(shuttle, - _centCom.Value, _consoleAccumulator, TransitTime, dock: true); + CentCom.Value, _consoleAccumulator, TransitTime, dock: true); } } } @@ -169,8 +169,8 @@ public sealed partial class ShuttleSystem Timer.Spawn((int) (TransitTime * 1000) + _bufferTime.Milliseconds, () => _roundEnd.EndRound(), _roundEndCancelToken.Token); // Guarantees that emergency shuttle arrives first before anyone else can FTL. - if (_centCom != null) - AddFTLDestination(_centCom.Value, true); + if (CentCom != null) + AddFTLDestination(CentCom.Value, true); } } diff --git a/Content.Server/Shuttles/Systems/ShuttleSystem.EmergencyShuttle.cs b/Content.Server/Shuttles/Systems/ShuttleSystem.EmergencyShuttle.cs index 43885e6151..5a5f154656 100644 --- a/Content.Server/Shuttles/Systems/ShuttleSystem.EmergencyShuttle.cs +++ b/Content.Server/Shuttles/Systems/ShuttleSystem.EmergencyShuttle.cs @@ -38,8 +38,8 @@ public sealed partial class ShuttleSystem [Dependency] private readonly DockingSystem _dockSystem = default!; [Dependency] private readonly StationSystem _station = default!; - private MapId? _centComMap; - private EntityUid? _centCom; + public MapId? CentComMap { get; private set; } + public EntityUid? CentCom { get; private set; } /// /// Used for multiple shuttle spawn offsets. @@ -367,8 +367,8 @@ public sealed partial class ShuttleSystem _consoleAccumulator = _configManager.GetCVar(CCVars.EmergencyShuttleDockTime); EmergencyShuttleArrived = true; - if (_centComMap != null) - _mapManager.SetMapPaused(_centComMap.Value, false); + if (CentComMap != null) + _mapManager.SetMapPaused(CentComMap.Value, false); foreach (var comp in EntityQuery(true)) { @@ -394,21 +394,21 @@ public sealed partial class ShuttleSystem private void SetupEmergencyShuttle() { - if (!_emergencyShuttleEnabled || _centComMap != null && _mapManager.MapExists(_centComMap.Value)) return; + if (!_emergencyShuttleEnabled || CentComMap != null && _mapManager.MapExists(CentComMap.Value)) return; - _centComMap = _mapManager.CreateMap(); - _mapManager.SetMapPaused(_centComMap.Value, true); + CentComMap = _mapManager.CreateMap(); + _mapManager.SetMapPaused(CentComMap.Value, true); // Load CentCom var centComPath = _configManager.GetCVar(CCVars.CentcommMap); if (!string.IsNullOrEmpty(centComPath)) { - var (_, centcomm) = _loader.LoadGrid(_centComMap.Value, "/Maps/centcomm.yml"); - _centCom = centcomm; + var (_, centcomm) = _loader.LoadGrid(CentComMap.Value, "/Maps/centcomm.yml"); + CentCom = centcomm; - if (_centCom != null) - AddFTLDestination(_centCom.Value, false); + if (CentCom != null) + AddFTLDestination(CentCom.Value, false); } else { @@ -423,10 +423,10 @@ public sealed partial class ShuttleSystem private void AddEmergencyShuttle(StationDataComponent component) { - if (!_emergencyShuttleEnabled || _centComMap == null || component.EmergencyShuttle != null) return; + if (!_emergencyShuttleEnabled || CentComMap == null || component.EmergencyShuttle != null) return; // Load escape shuttle - var (_, shuttle) = _loader.LoadGrid(_centComMap.Value, component.EmergencyShuttlePath.ToString(), new MapLoadOptions() + var (_, shuttle) = _loader.LoadGrid(CentComMap.Value, component.EmergencyShuttlePath.ToString(), new MapLoadOptions() { // Should be far enough... right? I'm too lazy to bounds check CentCom rn. Offset = new Vector2(500f + _shuttleIndex, 0f) @@ -452,13 +452,13 @@ public sealed partial class ShuttleSystem _shuttleIndex = 0f; - if (_centComMap == null || !_mapManager.MapExists(_centComMap.Value)) + if (CentComMap == null || !_mapManager.MapExists(CentComMap.Value)) { - _centComMap = null; + CentComMap = null; return; } - _mapManager.DeleteMap(_centComMap.Value); + _mapManager.DeleteMap(CentComMap.Value); } /// diff --git a/Resources/Locale/en-US/game-ticking/game-presets/preset-nukeops.ftl b/Resources/Locale/en-US/game-ticking/game-presets/preset-nukeops.ftl index 3ad35d7b25..4becfeef9d 100644 --- a/Resources/Locale/en-US/game-ticking/game-presets/preset-nukeops.ftl +++ b/Resources/Locale/en-US/game-ticking/game-presets/preset-nukeops.ftl @@ -1,8 +1,26 @@ nukeops-title = Nuclear Operatives nukeops-description = Nuclear operatives have targeted the station. Try to keep them from arming and detonating the nuke by protecting the nuke disk! -nukeops-ops-won = The nuke exploded. The nuclear operatives were successful! -nukeops-crew-won = The crew managed to defeat the nuclear operatives. +nukeops-welcome = You are a nuclear operative. Your goal is to blow up {$station}, and ensure that it is nothing but a pile of rubble. Your bosses, the Syndicate, have provided you with the tools you'll need for the task. + +nukeops-opsmajor = Syndicate major victory! +nukeops-opsminor = Syndicate minor victory! +nukeops-neutral = Neutral outcome! +nukeops-crewminor = Crew minor victory! +nukeops-crewmajor = Crew major victory! + +nukeops-cond-nukeexplodedoncorrectstation = The nuclear operatives managed to blow up the station. +nukeops-cond-nukeexplodedonnukieoutpost = The nuclear operative outpost was destroyed by a nuclear blast. +nukeops-cond-nukeexplodedonincorrectlocation = The nuclear bomb was detonated off-station. +nukeops-cond-nukeactiveinstation = The nuclear bomb was left armed on-station. +nukeops-cond-nukeactiveatcentcom = The nuclear bomb was delivered to Central Command! +nukeops-cond-nukediskoncentcom = The crew escaped with the nuclear authentication disk. +nukeops-cond-nukedisknotoncentcom = The crew left the nuclear authentication disk behind. +nukeops-cond-nukiesabandoned = The nuclear operatives were abandoned. +nukeops-cond-allnukiesdead = All nuclear operatives have died. +nukeops-cond-somenukiesalive = Some nuclear operatives died. +nukeops-cond-allnukiesalive = No nuclear operatives died. + nukeops-list-start = The nuke ops were: nukeops-not-enough-ready-players = Not enough players readied up for the game! There were {$readyPlayersCount} players readied up out of {$minimumPlayers} needed. Can't start Nukeops. nukeops-no-one-ready = No players readied up! Can't start Nukeops.