diff --git a/Content.Server/GameObjects/Components/Mobs/MindComponent.cs b/Content.Server/GameObjects/Components/Mobs/MindComponent.cs index 1cfc3d9991..8138dfb5d6 100644 --- a/Content.Server/GameObjects/Components/Mobs/MindComponent.cs +++ b/Content.Server/GameObjects/Components/Mobs/MindComponent.cs @@ -118,9 +118,17 @@ namespace Content.Server.GameObjects.Components.Mobs if (!ShowExamineInfo) return; + var dead = false; + + if(Owner.TryGetComponent(out var species)) + if (species.CurrentDamageState is DeadState) + dead = true; + // TODO: Use gendered pronouns depending on the entity if(!HasMind) - message.AddMarkup($"[color=red]They are totally catatonic. The stresses of life in deep-space must have been too much for them. Any recovery is unlikely.[/color]"); + message.AddMarkup(!dead + ? $"[color=red]They are totally catatonic. The stresses of life in deep-space must have been too much for them. Any recovery is unlikely.[/color]" + : $"[color=purple]Their soul has departed.[/color]"); else if(Mind.Session == null) message.AddMarkup("[color=yellow]They have a blank, absent-minded stare and appears completely unresponsive to anything. They may snap out of it soon.[/color]"); } diff --git a/Content.Server/GameObjects/Components/Mobs/SpeciesComponent.cs b/Content.Server/GameObjects/Components/Mobs/SpeciesComponent.cs index c26c109d70..14df66475a 100644 --- a/Content.Server/GameObjects/Components/Mobs/SpeciesComponent.cs +++ b/Content.Server/GameObjects/Components/Mobs/SpeciesComponent.cs @@ -177,7 +177,7 @@ namespace Content.Server.GameObjects currentstate = threshold; - EntityEventArgs toRaise = new MobDamageStateChangedMessage(this); + var toRaise = new MobDamageStateChangedMessage(this); Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, toRaise); } diff --git a/Content.Server/GameTicking/GamePreset.cs b/Content.Server/GameTicking/GamePreset.cs index b880b162a0..0075f7ae1b 100644 --- a/Content.Server/GameTicking/GamePreset.cs +++ b/Content.Server/GameTicking/GamePreset.cs @@ -1,11 +1,14 @@ -namespace Content.Server.GameTicking +using System.Collections.Generic; +using Robust.Server.Interfaces.Player; + +namespace Content.Server.GameTicking { /// /// A round-start setup preset, such as which antagonists to spawn. /// public abstract class GamePreset { - public abstract void Start(); + public abstract bool Start(IReadOnlyList players); public virtual string ModeTitle => "Sandbox"; public virtual string Description => "Secret!"; } diff --git a/Content.Server/GameTicking/GamePresets/PresetDeathMatch.cs b/Content.Server/GameTicking/GamePresets/PresetDeathMatch.cs index 5b866dc8fa..f45fa125b3 100644 --- a/Content.Server/GameTicking/GamePresets/PresetDeathMatch.cs +++ b/Content.Server/GameTicking/GamePresets/PresetDeathMatch.cs @@ -1,5 +1,7 @@ -using Content.Server.GameTicking.GameRules; +using System.Collections.Generic; +using Content.Server.GameTicking.GameRules; using Content.Server.Interfaces.GameTicking; +using Robust.Server.Interfaces.Player; using Robust.Shared.IoC; namespace Content.Server.GameTicking.GamePresets @@ -10,9 +12,10 @@ namespace Content.Server.GameTicking.GamePresets [Dependency] private readonly IGameTicker _gameTicker; #pragma warning restore 649 - public override void Start() + public override bool Start(IReadOnlyList readyPlayers) { _gameTicker.AddGameRule(); + return true; } public override string ModeTitle => "Deathmatch"; diff --git a/Content.Server/GameTicking/GamePresets/PresetSandbox.cs b/Content.Server/GameTicking/GamePresets/PresetSandbox.cs index 05f15c6972..2eeab4a049 100644 --- a/Content.Server/GameTicking/GamePresets/PresetSandbox.cs +++ b/Content.Server/GameTicking/GamePresets/PresetSandbox.cs @@ -1,4 +1,6 @@ -using Content.Server.Sandbox; +using System.Collections.Generic; +using Content.Server.Sandbox; +using Robust.Server.Interfaces.Player; using Robust.Shared.IoC; namespace Content.Server.GameTicking.GamePresets @@ -9,9 +11,10 @@ namespace Content.Server.GameTicking.GamePresets [Dependency] private readonly ISandboxManager _sandboxManager; #pragma warning restore 649 - public override void Start() + public override bool Start(IReadOnlyList readyPlayers) { _sandboxManager.IsSandboxEnabled = true; + return true; } public override string ModeTitle => "Sandbox"; diff --git a/Content.Server/GameTicking/GamePresets/PresetSuspicion.cs b/Content.Server/GameTicking/GamePresets/PresetSuspicion.cs new file mode 100644 index 0000000000..d4692cecf8 --- /dev/null +++ b/Content.Server/GameTicking/GamePresets/PresetSuspicion.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Content.Server.GameTicking.GameRules; +using Content.Server.Interfaces.Chat; +using Content.Server.Interfaces.GameTicking; +using Content.Server.Mobs.Roles; +using Content.Server.Players; +using Content.Server.Sandbox; +using NFluidsynth; +using Robust.Server.Interfaces.Player; +using Robust.Shared.Interfaces.Random; +using Robust.Shared.IoC; +using Robust.Shared.Random; +using Logger = Robust.Shared.Log.Logger; + +namespace Content.Server.GameTicking.GamePresets +{ + public class PresetSuspicion : GamePreset + { +#pragma warning disable 649 + [Dependency] private readonly ISandboxManager _sandboxManager; + [Dependency] private readonly IChatManager _chatManager; + [Dependency] private readonly IGameTicker _gameTicker; + [Dependency] private readonly IRobustRandom _random; +#pragma warning restore 649 + + public int MinPlayers { get; set; } = 5; + public int MinTraitors { get; set; } = 2; + public int PlayersPerTraitor { get; set; } = 5; + + public override bool Start(IReadOnlyList readyPlayers) + { + if (readyPlayers.Count < MinPlayers) + { + _chatManager.DispatchServerAnnouncement($"Not enough players readied up for the game! There were {readyPlayers.Count} players readied up out of {MinPlayers} needed."); + return false; + } + + var list = new List(readyPlayers); + var numTraitors = Math.Max(readyPlayers.Count() % PlayersPerTraitor, MinTraitors); + + for (var i = 0; i < numTraitors; i++) + { + var traitor = _random.PickAndTake(list); + var mind = traitor.Data.ContentData().Mind; + mind.AddRole(new SuspicionTraitorRole(mind)); + } + + foreach (var player in list) + { + var mind = player.Data.ContentData().Mind; + mind.AddRole(new SuspicionInnocentRole(mind)); + } + + _gameTicker.AddGameRule(); + return true; + } + + public override string ModeTitle => "Suspicion"; + public override string Description => "Suspicion on the Space Station. There are traitors on board... Can you kill them before they kill you?"; + } +} diff --git a/Content.Server/GameTicking/GameRules/RuleSuspicion.cs b/Content.Server/GameTicking/GameRules/RuleSuspicion.cs new file mode 100644 index 0000000000..91b16f3aaf --- /dev/null +++ b/Content.Server/GameTicking/GameRules/RuleSuspicion.cs @@ -0,0 +1,122 @@ +using System; +using System.Threading; +using Content.Server.GameObjects; +using Content.Server.GameObjects.Components.Mobs; +using Content.Server.GameObjects.Components.Observer; +using Content.Server.Interfaces.Chat; +using Content.Server.Interfaces.GameTicking; +using Content.Server.Mobs.Roles; +using Content.Server.Players; +using NFluidsynth; +using Robust.Server.Interfaces.Player; +using Robust.Server.Player; +using Robust.Shared.Enums; +using Robust.Shared.GameObjects; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.IoC; +using Logger = Robust.Shared.Log.Logger; +using Timer = Robust.Shared.Timers.Timer; + +namespace Content.Server.GameTicking.GameRules +{ + /// + /// Simple GameRule that will do a free-for-all death match. + /// Kill everybody else to win. + /// + public sealed class RuleSuspicion : GameRule, IEntityEventSubscriber + { + private static readonly TimeSpan DeadCheckDelay = TimeSpan.FromSeconds(1); + +#pragma warning disable 649 + [Dependency] private readonly IPlayerManager _playerManager; + [Dependency] private readonly IChatManager _chatManager; + [Dependency] private readonly IEntityManager _entityManager; + [Dependency] private readonly IGameTicker _gameTicker; +#pragma warning restore 649 + + private readonly CancellationTokenSource _checkTimerCancel = new CancellationTokenSource(); + + public override void Added() + { + _chatManager.DispatchServerAnnouncement("There are traitors on the station! Find them, and kill them!"); + + _entityManager.EventBus.SubscribeEvent(EventSource.Local, this, _onMobDamageStateChanged); + + Timer.SpawnRepeating(DeadCheckDelay, _checkWinConditions, _checkTimerCancel.Token); + } + + private void _onMobDamageStateChanged(MobDamageStateChangedMessage message) + { + var owner = message.Species.Owner; + + if (!(message.Species.CurrentDamageState is DeadState)) + return; + + if (!owner.TryGetComponent(out var mind)) + return; + + if (!mind.HasMind) + return; + + message.Species.Owner.Description += + mind.Mind.HasRole() ? "\nThey were a traitor!" : "\nThey were an innocent!"; + } + + public override void Removed() + { + base.Removed(); + + _checkTimerCancel.Cancel(); + } + + private void _checkWinConditions() + { + var traitorsAlive = 0; + var innocentsAlive = 0; + + foreach (var playerSession in _playerManager.GetAllPlayers()) + { + if (playerSession.AttachedEntity == null + || !playerSession.AttachedEntity.TryGetComponent(out SpeciesComponent species)) + { + continue; + } + + if (!species.CurrentDamageState.IsConscious) + { + continue; + } + + if (playerSession.ContentData().Mind.HasRole()) + traitorsAlive++; + else + innocentsAlive++; + } + + if ((innocentsAlive + traitorsAlive) == 0) + { + _chatManager.DispatchServerAnnouncement("Everybody is dead, it's a stalemate!"); + EndRound(); + } + + else if (traitorsAlive == 0) + { + _chatManager.DispatchServerAnnouncement("The traitors are dead! The innocents win."); + EndRound(); + } + else if (innocentsAlive == 0) + { + _chatManager.DispatchServerAnnouncement("The innocents are dead! The traitors win."); + EndRound(); + } + } + + private void EndRound() + { + _gameTicker.EndRound(); + _chatManager.DispatchServerAnnouncement($"Restarting in 10 seconds."); + _checkTimerCancel.Cancel(); + Timer.Spawn(TimeSpan.FromSeconds(10), () => _gameTicker.RestartRound()); + } + } +} diff --git a/Content.Server/GameTicking/GameTicker.cs b/Content.Server/GameTicking/GameTicker.cs index 764c65633a..c95211205d 100644 --- a/Content.Server/GameTicking/GameTicker.cs +++ b/Content.Server/GameTicking/GameTicker.cs @@ -104,7 +104,8 @@ namespace Content.Server.GameTicking _configurationManager.RegisterCVar("game.lobbyenabled", false, CVar.ARCHIVE); _configurationManager.RegisterCVar("game.lobbyduration", 20, CVar.ARCHIVE); - _configurationManager.RegisterCVar("game.defaultpreset", "Sandbox", CVar.ARCHIVE); + _configurationManager.RegisterCVar("game.defaultpreset", "Suspicion", CVar.ARCHIVE); + _configurationManager.RegisterCVar("game.fallbackpreset", "Sandbox", CVar.ARCHIVE); _playerManager.PlayerStatusChanged += _handlePlayerStatusChanged; @@ -181,11 +182,6 @@ namespace Content.Server.GameTicking SendServerMessage("The round is starting now..."); - RunLevel = GameRunLevel.InRound; - - var preset = MakeGamePreset(); - preset.Start(); - List readyPlayers; if (LobbyEnabled) { @@ -196,6 +192,8 @@ namespace Content.Server.GameTicking readyPlayers = _playersInLobby.Keys.ToList(); } + RunLevel = GameRunLevel.InRound; + // Get the profiles for each player for easier lookup. var profiles = readyPlayers.ToDictionary(p => p, GetPlayerProfile); @@ -222,6 +220,18 @@ namespace Content.Server.GameTicking SpawnPlayer(player, job, false); } + // Time to start the preset. + var preset = MakeGamePreset(); + + if (!preset.Start(assignedJobs.Keys.ToList())) + { + SetStartPreset(_configurationManager.GetCVar("game.fallbackpreset")); + var newPreset = MakeGamePreset(); + _chatManager.DispatchServerAnnouncement($"Failed to start {preset.ModeTitle} mode! Defaulting to {newPreset.ModeTitle}..."); + if(!newPreset.Start(readyPlayers)) + throw new ApplicationException("Fallback preset failed to start!"); + } + _roundStartTimeSpan = IoCManager.Resolve().RealTime; _sendStatusToAll(); } @@ -255,15 +265,16 @@ namespace Content.Server.GameTicking var listOfPlayerInfo = new List(); foreach(var ply in _playerManager.GetAllPlayers().OrderBy(p => p.Name)) { - if(ply.AttachedEntity.TryGetComponent(out var mindComponent) - && mindComponent.HasMind) + var mind = ply.ContentData().Mind; + if(mind != null) { + var antag = mind.AllRoles.Any(role => role.Antag); var playerEndRoundInfo = new RoundEndPlayerInfo() { PlayerOOCName = ply.Name, - PlayerICName = mindComponent.Mind.CurrentEntity.Name, - Role = mindComponent.Mind.AllRoles.FirstOrDefault()?.Name ?? Loc.GetString("Unkown"), - Antag = false + PlayerICName = mind.CurrentEntity.Name, + Role = antag ? mind.AllRoles.First(role => role.Antag).Name : mind.AllRoles.FirstOrDefault()?.Name ?? Loc.GetString("Unkown"), + Antag = antag }; listOfPlayerInfo.Add(playerEndRoundInfo); } @@ -339,6 +350,7 @@ namespace Content.Server.GameTicking { "Sandbox" => typeof(PresetSandbox), "DeathMatch" => typeof(PresetDeathMatch), + "Suspicion" => typeof(PresetSuspicion), _ => throw new NotSupportedException() }); diff --git a/Content.Server/Mobs/Mind.cs b/Content.Server/Mobs/Mind.cs index e931b80fbc..5e631f68ec 100644 --- a/Content.Server/Mobs/Mind.cs +++ b/Content.Server/Mobs/Mind.cs @@ -1,12 +1,15 @@ using System; using System.Collections.Generic; +using System.Linq; using Content.Server.GameObjects.Components.Mobs; +using Content.Server.GameObjects.EntitySystems; using Content.Server.Players; using Robust.Server.Interfaces.GameObjects; using Robust.Server.Interfaces.Player; using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Network; +using Robust.Shared.Utility; using Robust.Shared.ViewVariables; namespace Content.Server.Mobs @@ -130,6 +133,13 @@ namespace Content.Server.Mobs _roles.Remove(role); } + public bool HasRole() where T : Role + { + var t = typeof(T); + + return _roles.Any(role => role.GetType() == t); + } + /// /// Transfer this mind's control over to a new entity. /// diff --git a/Content.Server/Mobs/Role.cs b/Content.Server/Mobs/Role.cs index c7f0c617a7..f78b579f11 100644 --- a/Content.Server/Mobs/Role.cs +++ b/Content.Server/Mobs/Role.cs @@ -1,6 +1,9 @@ // Hey look, // Antag Datums. +using Content.Server.GameObjects.EntitySystems; +using Robust.Shared.Utility; + namespace Content.Server.Mobs { /// @@ -20,6 +23,11 @@ namespace Content.Server.Mobs /// public abstract string Name { get; } + /// + /// Whether this role should be considered antagonistic or not. + /// + public abstract bool Antag { get; } + protected Role(Mind mind) { Mind = mind; diff --git a/Content.Server/Mobs/Roles/Job.cs b/Content.Server/Mobs/Roles/Job.cs index 53c7ca721c..680a91ff20 100644 --- a/Content.Server/Mobs/Roles/Job.cs +++ b/Content.Server/Mobs/Roles/Job.cs @@ -11,8 +11,9 @@ namespace Content.Server.Mobs.Roles public JobPrototype Prototype { get; } public override string Name { get; } + public override bool Antag => false; - public String StartingGear => Prototype.StartingGear; + public string StartingGear => Prototype.StartingGear; public Job(Mind mind, JobPrototype jobPrototype) : base(mind) { @@ -25,9 +26,7 @@ namespace Content.Server.Mobs.Roles base.Greet(); var chat = IoCManager.Resolve(); - chat.DispatchServerMessage( - Mind.Session, - String.Format("You're a new {0}. Do your best!", Name)); + chat.DispatchServerMessage(Mind.Session, $"You're a new {Name}. Do your best!"); } } diff --git a/Content.Server/Mobs/Roles/SuspicionInnocentRole.cs b/Content.Server/Mobs/Roles/SuspicionInnocentRole.cs new file mode 100644 index 0000000000..177a8d3bbd --- /dev/null +++ b/Content.Server/Mobs/Roles/SuspicionInnocentRole.cs @@ -0,0 +1,25 @@ +using Content.Server.GameObjects; +using Content.Server.Interfaces.Chat; +using Robust.Shared.IoC; +using Robust.Shared.Utility; + +namespace Content.Server.Mobs.Roles +{ + public class SuspicionInnocentRole : Role + { + public SuspicionInnocentRole(Mind mind) : base(mind) + { + } + + public override string Name => "Innocent"; + public override bool Antag => false; + + public override void Greet() + { + base.Greet(); + + var chat = IoCManager.Resolve(); + chat.DispatchServerMessage(Mind.Session, "You're an innocent!"); + } + } +} diff --git a/Content.Server/Mobs/Roles/SuspicionTraitorRole.cs b/Content.Server/Mobs/Roles/SuspicionTraitorRole.cs new file mode 100644 index 0000000000..65c1e10306 --- /dev/null +++ b/Content.Server/Mobs/Roles/SuspicionTraitorRole.cs @@ -0,0 +1,25 @@ +using Content.Server.GameObjects; +using Content.Server.Interfaces.Chat; +using Robust.Shared.IoC; +using Robust.Shared.Utility; + +namespace Content.Server.Mobs.Roles +{ + public sealed class SuspicionTraitorRole : Role + { + public SuspicionTraitorRole(Mind mind) : base(mind) + { + } + + public override string Name => "Traitor"; + public override bool Antag => true; + + public override void Greet() + { + base.Greet(); + + var chat = IoCManager.Resolve(); + chat.DispatchServerMessage(Mind.Session, "You're a traitor!"); + } + } +} diff --git a/Content.Server/Mobs/Roles/Traitor.cs b/Content.Server/Mobs/Roles/Traitor.cs deleted file mode 100644 index 16804d497f..0000000000 --- a/Content.Server/Mobs/Roles/Traitor.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Content.Server.Interfaces.Chat; -using Robust.Shared.IoC; - -namespace Content.Server.Mobs.Roles -{ - public sealed class Traitor : Role - { - public Traitor(Mind mind) : base(mind) - { - } - - public override string Name => "Traitor"; - - public override void Greet() - { - base.Greet(); - - var chat = IoCManager.Resolve(); - chat.DispatchServerMessage( - Mind.Session, - "You're a traitor. Go fuck something up. Or something. I don't care to be honest."); - } - } -} diff --git a/Content.Shared/SharedGameTicker.cs b/Content.Shared/SharedGameTicker.cs index 38d3e0985e..565a39ee70 100644 --- a/Content.Shared/SharedGameTicker.cs +++ b/Content.Shared/SharedGameTicker.cs @@ -138,7 +138,7 @@ namespace Content.Shared public string GamemodeTitle; public TimeSpan RoundDuration; - + public uint PlayerCount;