diff --git a/Content.Server/GameTicking/Rules/Components/NukeOperativeComponent.cs b/Content.Server/GameTicking/Rules/Components/NukeOperativeComponent.cs new file mode 100644 index 0000000000..23994f673a --- /dev/null +++ b/Content.Server/GameTicking/Rules/Components/NukeOperativeComponent.cs @@ -0,0 +1,10 @@ +namespace Content.Server.GameTicking.Rules.Components; + +/// +/// This is used for tagging a mob as a nuke operative. +/// +[RegisterComponent] +public sealed class NukeOperativeComponent : Component +{ + +} diff --git a/Content.Server/GameTicking/Rules/Components/NukeOperativeSpawnerComponent.cs b/Content.Server/GameTicking/Rules/Components/NukeOperativeSpawnerComponent.cs new file mode 100644 index 0000000000..198db1f912 --- /dev/null +++ b/Content.Server/GameTicking/Rules/Components/NukeOperativeSpawnerComponent.cs @@ -0,0 +1,20 @@ +namespace Content.Server.GameTicking.Rules.Components; + +/// +/// This is used for tagging a spawn point as a nuke operative spawn point +/// and providing loadout + name for the operative on spawn. +/// TODO: Remove once systems can request spawns from the ghost role system directly. +/// +[RegisterComponent] +[Access(typeof(NukeopsRuleSystem))] +public sealed class NukeOperativeSpawnerComponent : Component +{ + [DataField("name")] + public string OperativeName = ""; + + [DataField("rolePrototype")] + public string OperativeRolePrototype = ""; + + [DataField("startingGearPrototype")] + public string OperativeStartingGear = ""; +} diff --git a/Content.Server/GameTicking/Rules/Configurations/NukeopsRuleConfiguration.cs b/Content.Server/GameTicking/Rules/Configurations/NukeopsRuleConfiguration.cs new file mode 100644 index 0000000000..b3ced1f989 --- /dev/null +++ b/Content.Server/GameTicking/Rules/Configurations/NukeopsRuleConfiguration.cs @@ -0,0 +1,63 @@ +using Content.Server.GameTicking.Rules.Configurations; +using Content.Shared.Dataset; +using Content.Shared.Roles; +using Robust.Shared.Audio; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Robust.Shared.Utility; + +namespace Content.Server.GameTicking.Rules.Configurations; + +public sealed class NukeopsRuleConfiguration : GameRuleConfiguration +{ + public override string Id => "Nukeops"; + + [DataField("minPlayers")] + public int MinPlayers = 15; + + [DataField("playersPerOperative")] + public int PlayersPerOperative = 5; + + [DataField("maxOps")] + public int MaxOperatives = 5; + + [DataField("spawnEntityProto", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string SpawnEntityPrototype = "MobHumanNukeOp"; + + [DataField("spawnPointProto", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string SpawnPointPrototype = "SpawnPointNukies"; + + [DataField("ghostSpawnPointProto", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string GhostSpawnPointProto = "SpawnPointGhostNukeOperative"; + + [DataField("commanderRoleProto", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string CommanderRolePrototype = "NukeopsCommander"; + + [DataField("operativeRoleProto", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string OperativeRoleProto = "Nukeops"; + + [DataField("commanderStartingGearProto", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string CommanderStartGearPrototype = "SyndicateCommanderGearFull"; + + [DataField("medicStartGearProto", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string MedicStartGearPrototype = "SyndicateOperativeMedicFull"; + + [DataField("operativeStartGearProto", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string OperativeStartGearPrototype = "SyndicateOperativeGearFull"; + + [DataField("eliteNames", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string EliteNames = "SyndicateNamesElite"; + + [DataField("normalNames", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string NormalNames = "SyndicateNamesNormal"; + + [DataField("outpostMap", customTypeSerializer: typeof(ResourcePathSerializer))] + public ResourcePath? NukieOutpostMap = new("/Maps/nukieplanet.yml"); + + [DataField("shuttleMap", customTypeSerializer: typeof(ResourcePathSerializer))] + public ResourcePath? NukieShuttleMap = new("/Maps/infiltrator.yml"); + + [DataField("greetingSound", customTypeSerializer: typeof(SoundSpecifierTypeSerializer))] + public SoundSpecifier? GreetSound = new SoundPathSpecifier("/Audio/Misc/nukeops.ogg"); +} diff --git a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs index bf748a9aa1..16a4ba003c 100644 --- a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs @@ -1,27 +1,28 @@ 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; +using Content.Server.Mind.Components; using Content.Server.Nuke; -using Content.Server.Players; using Content.Server.RoundEnd; using Content.Server.Shuttles.Components; using Content.Server.Shuttles.Systems; using Content.Server.Spawners.Components; using Content.Server.Station.Systems; -using Content.Shared.CCVar; using Content.Shared.MobState; using Content.Shared.Dataset; using Content.Shared.Roles; using Robust.Server.Maps; using Robust.Server.Player; -using Robust.Shared.Configuration; using Robust.Shared.Map; using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Utility; using Content.Server.Traitor; -using System.Data; -using Content.Server.Traitor.Uplink; +using Content.Shared.MobState.Components; using Robust.Shared.Audio; using Robust.Shared.Player; @@ -31,23 +32,44 @@ 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 RoundEndSystem _roundEndSystem = default!; - [Dependency] private readonly UplinkSystem _uplink = default!; + [Dependency] private readonly IPlayerManager _playerSystem = default!; + [Dependency] private readonly SharedAudioSystem _audioSystem = default!; - private Dictionary _aliveNukeops = new(); private bool _opsWon; + private MapId? _nukiePlanet; + private EntityUid? _nukieOutpost; + private EntityUid? _nukieShuttle; + public override string Prototype => "Nukeops"; - private readonly SoundSpecifier _greetSound = new SoundPathSpecifier("/Audio/Misc/nukeops.ogg"); + private NukeopsRuleConfiguration _nukeopsRuleConfig = new(); + + /// + /// Cached starting gear prototypes. + /// + private readonly Dictionary _startingGearPrototypes = new (); + + /// + /// Cached operator name prototypes. + /// + private readonly Dictionary> _operativeNames = new(); + + /// + /// Data to be used in for an operative once the Mind has been added. + /// + private readonly Dictionary _operativeMindPendingData = new(); + + /// + /// Players who played as an operative at some point in the round. + /// + private readonly HashSet _operativePlayers = new(); - private const string NukeopsPrototypeId = "Nukeops"; - private const string NukeopsCommanderPrototypeId = "NukeopsCommander"; public override void Initialize() { @@ -55,9 +77,29 @@ public sealed class NukeopsRuleSystem : GameRuleSystem SubscribeLocalEvent(OnStartAttempt); SubscribeLocalEvent(OnPlayersSpawning); - SubscribeLocalEvent(OnMobStateChanged); + SubscribeLocalEvent(OnMobStateChanged); SubscribeLocalEvent(OnRoundEndText); SubscribeLocalEvent(OnNukeExploded); + SubscribeLocalEvent(OnPlayersGhostSpawning); + SubscribeLocalEvent(OnMindAdded); + SubscribeLocalEvent(OnComponentInit); + SubscribeLocalEvent(OnComponentRemove); + } + + private void OnComponentInit(EntityUid uid, NukeOperativeComponent component, ComponentInit args) + { + // If entity has a prior mind attached, add them to the players list. + if (!TryComp(uid, out var mindComponent) || !RuleAdded) + return; + + var session = mindComponent.Mind?.Session; + if (session != null) + _operativePlayers.Add(session); + } + + private void OnComponentRemove(EntityUid uid, NukeOperativeComponent component, ComponentRemove args) + { + CheckRoundShouldEnd(); } private void OnNukeExploded(NukeExplodedEvent ev) @@ -76,25 +118,44 @@ public sealed class NukeopsRuleSystem : GameRuleSystem ev.AddLine(_opsWon ? Loc.GetString("nukeops-ops-won") : Loc.GetString("nukeops-crew-won")); ev.AddLine(Loc.GetString("nukeops-list-start")); - foreach (var nukeop in _aliveNukeops) + foreach (var nukeop in _operativePlayers) { - ev.AddLine($"- {nukeop.Key.Session?.Name}"); + ev.AddLine($"- {nukeop.Name}"); } } - private void OnMobStateChanged(MobStateChangedEvent ev) + private void CheckRoundShouldEnd() { if (!RuleAdded) return; - if (!_aliveNukeops.TryFirstOrNull(x => x.Key.OwnedEntity == ev.Entity, out var op)) return; + MapId? shuttleMapId = EntityManager.EntityExists(_nukieShuttle) + ? Transform(_nukieShuttle!.Value).MapID + : null; - _aliveNukeops[op.Value.Key] = op.Value.Key.CharacterDeadIC; + // Check if there are nuke operatives still alive on the same map as the shuttle. + // If there are, the round can continue. + var operatives = EntityQuery(true); + var operativesAlive = operatives + .Where(ent => ent.Item3.MapID == shuttleMapId) + .Any(ent => ent.Item2.CurrentState == DamageState.Alive && ent.Item1.Running); - if (_aliveNukeops.Values.All(x => !x)) - { - _roundEndSystem.EndRound(); - } + if (operativesAlive) + return; // There are living operatives than can access the shuttle. + + // 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. + _roundEndSystem.EndRound(); + } + + private void OnMobStateChanged(EntityUid uid, NukeOperativeComponent component, MobStateChangedEvent ev) + { + if(ev.CurrentMobState == DamageState.Dead) + CheckRoundShouldEnd(); } private void OnPlayersSpawning(RulePlayerSpawningEvent ev) @@ -102,11 +163,9 @@ public sealed class NukeopsRuleSystem : GameRuleSystem if (!RuleAdded) return; - _aliveNukeops.Clear(); - // Basically copied verbatim from traitor code - var playersPerOperative = _cfg.GetCVar(CCVars.NukeopsPlayersPerOp); - var maxOperatives = _cfg.GetCVar(CCVars.NukeopsMaxOps); + var playersPerOperative = _nukeopsRuleConfig.PlayersPerOperative; + var maxOperatives = _nukeopsRuleConfig.MaxOperatives; var everyone = new List(ev.PlayerPool); var prefList = new List(); @@ -122,11 +181,11 @@ public sealed class NukeopsRuleSystem : GameRuleSystem continue; } var profile = ev.Profiles[player.UserId]; - if (profile.AntagPreferences.Contains(NukeopsPrototypeId)) + if (profile.AntagPreferences.Contains(_nukeopsRuleConfig.OperativeRoleProto)) { prefList.Add(player); } - if (profile.AntagPreferences.Contains(NukeopsCommanderPrototypeId)) + if (profile.AntagPreferences.Contains(_nukeopsRuleConfig.CommanderRolePrototype)) { cmdrPrefList.Add(player); } @@ -189,118 +248,226 @@ public sealed class NukeopsRuleSystem : GameRuleSystem operatives.Add(nukeOp); } - // TODO: Make this a prototype - // so true PAUL! - var path = "/Maps/nukieplanet.yml"; - var shuttlePath = "/Maps/infiltrator.yml"; - var mapId = _mapManager.CreateMap(); + SpawnOperatives(numNukies, operatives, false); - var (_, outpost) = _mapLoader.LoadGrid(mapId, "/Maps/nukieplanet.yml"); - - if (outpost == null) + foreach(var session in operatives) { - Logger.ErrorS("nukies", $"Error loading map {path} for nukies!"); - return; + ev.PlayerPool.Remove(session); + GameTicker.PlayerJoinGame(session); + _operativePlayers.Add(session); } + if (_nukeopsRuleConfig.GreetSound == null) + return; + + _audioSystem.PlayGlobal(_nukeopsRuleConfig.GreetSound, Filter.Empty().AddPlayers(operatives), AudioParams.Default); + } + + private void OnPlayersGhostSpawning(EntityUid uid, NukeOperativeComponent component, GhostRoleSpawnerUsedEvent args) + { + var spawner = args.Spawner; + + if (!TryComp(spawner, out var nukeOpSpawner)) + return; + + SetupOperativeEntity(uid, nukeOpSpawner.OperativeName, nukeOpSpawner.OperativeStartingGear); + + _operativeMindPendingData.Add(uid, nukeOpSpawner.OperativeRolePrototype); + } + + private void OnMindAdded(EntityUid uid, NukeOperativeComponent component, MindAddedMessage args) + { + if (!TryComp(uid, out var mindComponent) || mindComponent.Mind == null) + return; + + var mind = mindComponent.Mind; + + if (_operativeMindPendingData.TryGetValue(uid, out var role)) + { + mind.AddRole(new TraitorRole(mind, _prototypeManager.Index(role))); + _operativeMindPendingData.Remove(uid); + } + + if (!mind.TryGetSession(out var playerSession)) + return; + + _operativePlayers.Add(playerSession); + + if (_nukeopsRuleConfig.GreetSound != null) + _audioSystem.PlayGlobal(_nukeopsRuleConfig.GreetSound, Filter.Empty().AddPlayer(playerSession), AudioParams.Default); + } + + private bool SpawnMap() + { + if (_nukiePlanet != null) + return true; // Map is already loaded. + + var path = _nukeopsRuleConfig.NukieOutpostMap; + var shuttlePath = _nukeopsRuleConfig.NukieShuttleMap; + if (path == null) + { + Logger.ErrorS("nukies", "No station map specified for nukeops!"); + return false; + } + + if (shuttlePath == null) + { + Logger.ErrorS("nukies", "No shuttle map specified for nukeops!"); + return false; + } + + var mapId = _mapManager.CreateMap(); + + var (_, outpostGrids) = _mapLoader.LoadMap(mapId, path.ToString()); + if (outpostGrids.Count == 0) + { + Logger.ErrorS("nukies", $"Error loading map {path} for nukies!"); + return false; + } + + // Assume the first grid is the outpost grid. + _nukieOutpost = outpostGrids[0]; + // Listen I just don't want it to overlap. - var (_, shuttleId) = _mapLoader.LoadGrid(mapId, shuttlePath, new MapLoadOptions() + var (_, shuttleId) = _mapLoader.LoadGrid(mapId, shuttlePath.ToString(), new MapLoadOptions() { Offset = Vector2.One * 1000f, }); // Naughty, someone saved the shuttle as a map. - if (Deleted(outpost)) + if (Deleted(shuttleId)) { Logger.ErrorS("nukeops", $"Tried to load nukeops shuttle as a map, aborting."); _mapManager.DeleteMap(mapId); - return; + return false; } if (TryComp(shuttleId, out var shuttle)) { - IoCManager.Resolve().GetEntitySystem().TryFTLDock(shuttle, outpost.Value); + IoCManager.Resolve().GetEntitySystem().TryFTLDock(shuttle, _nukieOutpost.Value); } - // TODO: Loot table or something - var commanderGear = _prototypeManager.Index("SyndicateCommanderGearFull"); - var starterGear = _prototypeManager.Index("SyndicateOperativeGearFull"); - var medicGear = _prototypeManager.Index("SyndicateOperativeMedicFull"); - var syndicateNamesElite = new List(_prototypeManager.Index("SyndicateNamesElite").Values); - var syndicateNamesNormal = new List(_prototypeManager.Index("SyndicateNamesNormal").Values); + _nukiePlanet = mapId; + _nukieShuttle = shuttleId; + return true; + } + + private (string Name, string Role, string Gear) GetOperativeSpawnDetails(int spawnNumber) + { + string name; + string role; + string gear; + + // Spawn the Commander then Agent first. + switch (spawnNumber) + { + case 0: + name = $"Commander " + _random.PickAndTake(_operativeNames[_nukeopsRuleConfig.EliteNames]); + role = _nukeopsRuleConfig.CommanderRolePrototype; + gear = _nukeopsRuleConfig.CommanderStartGearPrototype; + break; + case 1: + name = $"Agent " + _random.PickAndTake(_operativeNames[_nukeopsRuleConfig.NormalNames]); + role = _nukeopsRuleConfig.OperativeRoleProto; + gear = _nukeopsRuleConfig.MedicStartGearPrototype; + break; + default: + name = $"Operator " + _random.PickAndTake(_operativeNames[_nukeopsRuleConfig.NormalNames]); + role = _nukeopsRuleConfig.OperativeRoleProto; + gear = _nukeopsRuleConfig.OperativeStartGearPrototype; + break; + } + + return (name, role, gear); + } + + /// + /// Adds missing nuke operative components, equips starting gear and renames the entity. + /// + private void SetupOperativeEntity(EntityUid mob, string name, string gear) + { + EntityManager.GetComponent(mob).EntityName = name; + EntityManager.EnsureComponent(mob); + EntityManager.EnsureComponent(mob); + + if(_startingGearPrototypes.TryGetValue(gear, out var gearPrototype)) + _stationSpawningSystem.EquipStartingGear(mob, gearPrototype, null); + } + + private void SpawnOperatives(int spawnCount, List sessions, bool addSpawnPoints) + { + if (_nukieOutpost == null) + return; + + var outpostUid = _nukieOutpost.Value; var spawns = new List(); // Forgive me for hardcoding prototypes foreach (var (_, meta, xform) in EntityManager.EntityQuery(true)) { - if (meta.EntityPrototype?.ID != "SpawnPointNukies") continue; + if (meta.EntityPrototype?.ID != _nukeopsRuleConfig.SpawnPointPrototype) + continue; - if (xform.ParentUid == outpost) - { - spawns.Add(xform.Coordinates); - break; - } + if (xform.ParentUid != _nukieOutpost) + continue; + + spawns.Add(xform.Coordinates); + break; } if (spawns.Count == 0) { - spawns.Add(EntityManager.GetComponent(outpost.Value).Coordinates); + spawns.Add(EntityManager.GetComponent(outpostUid).Coordinates); Logger.WarningS("nukies", $"Fell back to default spawn for nukies!"); } // TODO: This should spawn the nukies in regardless and transfer if possible; rest should go to shot roles. - for (var i = 0; i < operatives.Count; i++) + for(var i = 0; i < spawnCount; i++) { - string name; - string role; - StartingGearPrototype gear; + var spawnDetails = GetOperativeSpawnDetails(i); + var nukeOpsAntag = _prototypeManager.Index(spawnDetails.Role); - switch (i) + if (sessions.TryGetValue(i, out var session)) { - case 0: - name = $"Commander " + _random.PickAndTake(syndicateNamesElite); - role = NukeopsCommanderPrototypeId; - gear = commanderGear; - break; - case 1: - name = $"Agent " + _random.PickAndTake(syndicateNamesNormal); - role = NukeopsPrototypeId; - gear = medicGear; - break; - default: - name = $"Operator " + _random.PickAndTake(syndicateNamesNormal); - role = NukeopsPrototypeId; - gear = starterGear; - break; + var mob = EntityManager.SpawnEntity(_nukeopsRuleConfig.SpawnEntityPrototype, _random.Pick(spawns)); + SetupOperativeEntity(mob, spawnDetails.Name, spawnDetails.Gear); + + var newMind = new Mind.Mind(session.UserId) + { + CharacterName = spawnDetails.Name + }; + newMind.ChangeOwningPlayer(session.UserId); + newMind.AddRole(new TraitorRole(newMind, nukeOpsAntag)); + + newMind.TransferTo(mob); } - - var session = operatives[i]; - var newMind = new Mind.Mind(session.UserId) + else if (addSpawnPoints) { - CharacterName = name - }; - newMind.ChangeOwningPlayer(session.UserId); - newMind.AddRole(new TraitorRole(newMind, _prototypeManager.Index(role))); + var spawnPoint = EntityManager.SpawnEntity(_nukeopsRuleConfig.GhostSpawnPointProto, _random.Pick(spawns)); + var spawner = EnsureComp(spawnPoint); + spawner.RoleName = nukeOpsAntag.Name; + spawner.RoleDescription = nukeOpsAntag.Objective; - var mob = EntityManager.SpawnEntity("MobHuman", _random.Pick(spawns)); - EntityManager.GetComponent(mob).EntityName = name; - EntityManager.AddComponent(mob); - - newMind.TransferTo(mob); - _stationSpawningSystem.EquipStartingGear(mob, gear, null); - - ev.PlayerPool.Remove(session); - _aliveNukeops.Add(newMind, true); - - GameTicker.PlayerJoinGame(session); + var nukeOpSpawner = EnsureComp(spawnPoint); + nukeOpSpawner.OperativeName = spawnDetails.Name; + nukeOpSpawner.OperativeRolePrototype = spawnDetails.Role; + nukeOpSpawner.OperativeStartingGear = spawnDetails.Gear; + } } + } - SoundSystem.Play(_greetSound.GetSound(), Filter.Empty().AddWhere(s => - { - var mind = ((IPlayerSession) s).Data.ContentData()?.Mind; - return mind != null && _aliveNukeops.ContainsKey(mind); - }), AudioParams.Default); + private void SpawnOperativesForGhostRoles() + { + // Basically copied verbatim from traitor code + var playersPerOperative = _nukeopsRuleConfig.PlayersPerOperative; + var maxOperatives = _nukeopsRuleConfig.MaxOperatives; + + var playerPool = _playerSystem.ServerSessions.ToList(); + var numNukies = MathHelper.Clamp(playerPool.Count / playersPerOperative, 1, maxOperatives); + + var operatives = new List(); + SpawnOperatives(numNukies, operatives, true); } //For admins forcing someone to nukeOps. @@ -309,7 +476,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem if (!mind.OwnedEntity.HasValue) return; - mind.AddRole(new TraitorRole(mind, _prototypeManager.Index(NukeopsPrototypeId))); + mind.AddRole(new TraitorRole(mind, _prototypeManager.Index(_nukeopsRuleConfig.OperativeRoleProto))); _stationSpawningSystem.EquipStartingGear(mind.OwnedEntity.Value, _prototypeManager.Index("SyndicateOperativeGearFull"), null); } @@ -318,7 +485,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem if (!RuleAdded) return; - var minPlayers = _cfg.GetCVar(CCVars.NukeopsMinPlayers); + var minPlayers = _nukeopsRuleConfig.MinPlayers; if (!ev.Forced && ev.Players.Length < minPlayers) { _chatManager.DispatchServerAnnouncement(Loc.GetString("nukeops-not-enough-ready-players", ("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers))); @@ -326,17 +493,60 @@ public sealed class NukeopsRuleSystem : GameRuleSystem return; } - if (ev.Players.Length == 0) - { - _chatManager.DispatchServerAnnouncement(Loc.GetString("nukeops-no-one-ready")); - ev.Cancel(); + if (ev.Players.Length != 0) return; - } + + _chatManager.DispatchServerAnnouncement(Loc.GetString("nukeops-no-one-ready")); + ev.Cancel(); } public override void Started() { _opsWon = false; + _nukieOutpost = null; + _nukiePlanet = null; + + _startingGearPrototypes.Clear(); + _operativeNames.Clear(); + _operativeMindPendingData.Clear(); + _operativePlayers.Clear(); + + if (Configuration is not NukeopsRuleConfiguration) + return; + + // TODO: Loot table or something + foreach (var proto in new[] + { + _nukeopsRuleConfig.CommanderStartGearPrototype, + _nukeopsRuleConfig.MedicStartGearPrototype, + _nukeopsRuleConfig.OperativeStartGearPrototype + }) + { + _startingGearPrototypes.Add(proto, _prototypeManager.Index(proto)); + } + + foreach (var proto in new[] { _nukeopsRuleConfig.EliteNames, _nukeopsRuleConfig.NormalNames }) + { + _operativeNames.Add(proto, new List(_prototypeManager.Index(proto).Values)); + } + + + if (!SpawnMap()) + { + Logger.InfoS("nukies", "Failed to load map for nukeops"); + return; + } + + // Add pre-existing nuke operatives to the credit list. + var query = EntityQuery(true); + foreach (var (_, mindComp) in query) + { + if (mindComp.Mind?.TryGetSession(out var session) == true) + _operativePlayers.Add(session); + } + + if (GameTicker.RunLevel == GameRunLevel.InRound) + SpawnOperativesForGhostRoles(); } public override void Ended() { } diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index 44c4bf26d3..4acaae3570 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -304,19 +304,6 @@ namespace Content.Shared.CCVar public static readonly CVarDef TraitorDeathMatchStartingBalance = CVarDef.Create("traitordm.starting_balance", 20); - /* - * Nukeops - */ - - public static readonly CVarDef NukeopsMinPlayers = - CVarDef.Create("nukeops.min_players", 15); - - public static readonly CVarDef NukeopsMaxOps = - CVarDef.Create("nukeops.max_ops", 5); - - public static readonly CVarDef NukeopsPlayersPerOp = - CVarDef.Create("nukeops.players_per_op", 5); - /* * Zombie */ diff --git a/Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml b/Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml index d655022da7..f9ab058673 100644 --- a/Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml +++ b/Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml @@ -49,3 +49,21 @@ - state: green - texture: Mobs/Pets/corgi.rsi/narsian.png +- type: entity + noSpawn: true + id: SpawnPointGhostNukeOperative + name: ghost role spawn point + suffix: nukeops + parent: MarkerBase + components: + - type: GhostRoleMobSpawner + prototype: MobHumanNukeOp + rules: You are a syndicate operative tasked with the destruction of the station. As an antagonist, do whatever is required to complete this task. + - type: NukeOperativeSpawner + - type: Sprite + sprite: Markers/jobs.rsi + layers: + - state: green + - texture: Structures/Wallmounts/signs.rsi/radiation.png + + diff --git a/Resources/Prototypes/Entities/Mobs/Player/human.yml b/Resources/Prototypes/Entities/Mobs/Player/human.yml index 27e6757b5e..3cf73c5df5 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/human.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/human.yml @@ -189,7 +189,6 @@ - type: Loadout prototype: ERTJanitorGearEVA - #Syndie - type: entity parent: MobHuman @@ -200,3 +199,13 @@ prototype: SyndicateOperativeGearExtremelyBasic - type: RandomMetadata nameSet: names_death_commando + +# Nuclear Operative +- type: entity + noSpawn: true + name: Nuclear Operative + parent: MobHuman + id: MobHumanNukeOp + components: + - type: NukeOperative + - type: RandomHumanoidAppearance diff --git a/Resources/Prototypes/GameRules/roundstart.yml b/Resources/Prototypes/GameRules/roundstart.yml index d1f636dcc0..b7f49e5421 100644 --- a/Resources/Prototypes/GameRules/roundstart.yml +++ b/Resources/Prototypes/GameRules/roundstart.yml @@ -21,7 +21,7 @@ - type: gameRule id: Nukeops config: - !type:GenericGameRuleConfiguration + !type:NukeopsRuleConfiguration id: Nukeops - type: gameRule