Refactor Game Modes (#5857)
This commit is contained in:
committed by
GitHub
parent
d1a1ee3cbe
commit
f4d8ec1b35
@@ -1,107 +0,0 @@
|
||||
#nullable enable annotations
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.Ghost.Components;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.Ghost;
|
||||
using Content.Shared.MobState.Components;
|
||||
using Content.Shared.Preferences;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.GameTicking.Presets
|
||||
{
|
||||
/// <summary>
|
||||
/// A round-start setup preset, such as which antagonists to spawn.
|
||||
/// </summary>
|
||||
public abstract class GamePreset
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entities = default!;
|
||||
|
||||
public abstract bool Start(IReadOnlyList<IPlayerSession> readyPlayers, bool force = false);
|
||||
public virtual string ModeTitle => "Sandbox";
|
||||
public virtual string Description => "Secret!";
|
||||
public virtual bool DisallowLateJoin => false;
|
||||
public Dictionary<NetUserId, HumanoidCharacterProfile> ReadyProfiles = new();
|
||||
|
||||
public virtual void OnGameStarted() { }
|
||||
|
||||
/// <summary>
|
||||
/// Called when a player is spawned in (this includes, but is not limited to, before Start)
|
||||
/// </summary>
|
||||
public virtual void OnSpawnPlayerCompleted(IPlayerSession session, EntityUid mob, bool lateJoin) { }
|
||||
|
||||
/// <summary>
|
||||
/// Called when a player attempts to ghost.
|
||||
/// </summary>
|
||||
public virtual bool OnGhostAttempt(Mind.Mind mind, bool canReturnGlobal)
|
||||
{
|
||||
var playerEntity = mind.OwnedEntity;
|
||||
|
||||
var entities = IoCManager.Resolve<IEntityManager>();
|
||||
if (entities.HasComponent<GhostComponent>(playerEntity))
|
||||
return false;
|
||||
|
||||
if (mind.VisitingEntity != default)
|
||||
{
|
||||
mind.UnVisit();
|
||||
}
|
||||
|
||||
var position = playerEntity is {Valid: true}
|
||||
? _entities.GetComponent<TransformComponent>(playerEntity.Value).Coordinates
|
||||
: EntitySystem.Get<GameTicker>().GetObserverSpawnPoint();
|
||||
// Ok, so, this is the master place for the logic for if ghosting is "too cheaty" to allow returning.
|
||||
// There's no reason at this time to move it to any other place, especially given that the 'side effects required' situations would also have to be moved.
|
||||
// + If CharacterDeadPhysically applies, we're physically dead. Therefore, ghosting OK, and we can return (this is critical for gibbing)
|
||||
// Note that we could theoretically be ICly dead and still physically alive and vice versa.
|
||||
// (For example, a zombie could be dead ICly, but may retain memories and is definitely physically active)
|
||||
// + If we're in a mob that is critical, and we're supposed to be able to return if possible,
|
||||
/// we're succumbing - the mob is killed. Therefore, character is dead. Ghosting OK.
|
||||
// (If the mob survives, that's a bug. Ghosting is kept regardless.)
|
||||
var canReturn = canReturnGlobal && mind.CharacterDeadPhysically;
|
||||
|
||||
if (canReturnGlobal && entities.TryGetComponent(playerEntity, out MobStateComponent? mobState))
|
||||
{
|
||||
if (mobState.IsCritical())
|
||||
{
|
||||
canReturn = true;
|
||||
|
||||
//todo: what if they dont breathe lol
|
||||
//cry deeply
|
||||
DamageSpecifier damage = new(IoCManager.Resolve<IPrototypeManager>().Index<DamageTypePrototype>("Asphyxiation"), 200);
|
||||
EntitySystem.Get<DamageableSystem>().TryChangeDamage(playerEntity, damage, true);
|
||||
}
|
||||
}
|
||||
|
||||
var ghost = entities.SpawnEntity("MobObserver", position.ToMap(entities));
|
||||
|
||||
// Try setting the ghost entity name to either the character name or the player name.
|
||||
// If all else fails, it'll default to the default entity prototype name, "observer".
|
||||
// However, that should rarely happen.
|
||||
if(!string.IsNullOrWhiteSpace(mind.CharacterName))
|
||||
entities.GetComponent<MetaDataComponent>(ghost).EntityName = mind.CharacterName;
|
||||
else if (!string.IsNullOrWhiteSpace(mind.Session?.Name))
|
||||
entities.GetComponent<MetaDataComponent>(ghost).EntityName = mind.Session.Name;
|
||||
|
||||
var ghostComponent = entities.GetComponent<GhostComponent>(ghost);
|
||||
|
||||
if (mind.TimeOfDeath.HasValue)
|
||||
{
|
||||
ghostComponent.TimeOfDeath = mind.TimeOfDeath!.Value;
|
||||
}
|
||||
|
||||
EntitySystem.Get<SharedGhostSystem>().SetCanReturnToBody(ghostComponent, canReturn);
|
||||
|
||||
if (canReturn)
|
||||
mind.Visit(ghost);
|
||||
else
|
||||
mind.TransferTo(ghost);
|
||||
return true;
|
||||
}
|
||||
|
||||
public virtual string GetRoundEndDescription() => string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Content.Server.GameTicking.Presets
|
||||
{
|
||||
/// <summary>
|
||||
/// Attribute that marks a game preset.
|
||||
/// The id and aliases are registered in lowercase in <see cref="GameTicker"/>.
|
||||
/// A duplicate id or alias will throw an exception.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
[BaseTypeRequired(typeof(GamePreset))]
|
||||
[MeansImplicitUse]
|
||||
public class GamePresetAttribute : Attribute
|
||||
{
|
||||
public string Id { get; }
|
||||
|
||||
public ImmutableList<string> Aliases { get; }
|
||||
|
||||
public GamePresetAttribute(string id, params string[] aliases)
|
||||
{
|
||||
Id = id;
|
||||
Aliases = aliases.ToImmutableList();
|
||||
}
|
||||
}
|
||||
}
|
||||
34
Content.Server/GameTicking/Presets/GamePresetPrototype.cs
Normal file
34
Content.Server/GameTicking/Presets/GamePresetPrototype.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.GameTicking.Rules;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
|
||||
|
||||
namespace Content.Server.GameTicking.Presets
|
||||
{
|
||||
/// <summary>
|
||||
/// A round-start setup preset, such as which antagonists to spawn.
|
||||
/// </summary>
|
||||
[Prototype("gamePreset")]
|
||||
public class GamePresetPrototype : IPrototype
|
||||
{
|
||||
[DataField("id", required:true)]
|
||||
public string ID { get; } = default!;
|
||||
|
||||
[DataField("alias")]
|
||||
public string[] Alias { get; } = Array.Empty<string>();
|
||||
|
||||
[DataField("name")]
|
||||
public string ModeTitle { get; } = "????";
|
||||
|
||||
[DataField("description")]
|
||||
public string Description { get; } = string.Empty;
|
||||
|
||||
[DataField("showInVote")]
|
||||
public bool ShowInVote { get; } = false;
|
||||
|
||||
[DataField("rules", customTypeSerializer:typeof(PrototypeIdListSerializer<GameRulePrototype>))]
|
||||
public IReadOnlyList<string> Rules { get; } = Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.GameTicking.Rules;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Content.Server.GameTicking.Presets
|
||||
{
|
||||
[GamePreset("deathmatch")]
|
||||
public sealed class PresetDeathMatch : GamePreset
|
||||
{
|
||||
public override bool Start(IReadOnlyList<IPlayerSession> readyPlayers, bool force = false)
|
||||
{
|
||||
EntitySystem.Get<GameTicker>().AddGameRule<RuleDeathMatch>();
|
||||
return true;
|
||||
}
|
||||
|
||||
public override string ModeTitle => "Deathmatch";
|
||||
public override string Description => "Kill anything that moves!";
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using Robust.Server.Player;
|
||||
|
||||
namespace Content.Server.GameTicking.Presets
|
||||
{
|
||||
[GamePreset("extended")]
|
||||
public class PresetExtended : GamePreset
|
||||
{
|
||||
public override string Description => "No antagonists, have fun!";
|
||||
public override string ModeTitle => "Extended";
|
||||
|
||||
public override bool Start(IReadOnlyList<IPlayerSession> readyPlayers, bool force = false)
|
||||
{
|
||||
// We do nothing. This is extended after all...
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.Sandbox;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Content.Server.GameTicking.Presets
|
||||
{
|
||||
[GamePreset("sandbox")]
|
||||
public sealed class PresetSandbox : GamePreset
|
||||
{
|
||||
[Dependency] private readonly ISandboxManager _sandboxManager = default!;
|
||||
|
||||
public override bool Start(IReadOnlyList<IPlayerSession> readyPlayers, bool force = false)
|
||||
{
|
||||
_sandboxManager.IsSandboxEnabled = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
public override string ModeTitle => "Sandbox";
|
||||
public override string Description => "No stress, build something!";
|
||||
}
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Server.GameTicking.Rules;
|
||||
using Content.Server.Players;
|
||||
using Content.Server.Suspicion;
|
||||
using Content.Server.Suspicion.Roles;
|
||||
using Content.Server.Traitor.Uplink;
|
||||
using Content.Server.Traitor.Uplink.Account;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Traitor.Uplink;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.GameTicking.Presets
|
||||
{
|
||||
[GamePreset("suspicion")]
|
||||
public class PresetSuspicion : GamePreset
|
||||
{
|
||||
[Dependency] private readonly IChatManager _chatManager = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] protected readonly IEntityManager EntityManager = default!;
|
||||
|
||||
public int MinPlayers { get; set; }
|
||||
public int MinTraitors { get; set; }
|
||||
public int PlayersPerTraitor { get; set; }
|
||||
|
||||
public int TraitorStartingBalance { get; set; }
|
||||
|
||||
|
||||
public override bool DisallowLateJoin => true;
|
||||
|
||||
private static string TraitorID = "SuspicionTraitor";
|
||||
private static string InnocentID = "SuspicionInnocent";
|
||||
|
||||
public override bool Start(IReadOnlyList<IPlayerSession> readyPlayers, bool force = false)
|
||||
{
|
||||
MinPlayers = _cfg.GetCVar(CCVars.SuspicionMinPlayers);
|
||||
MinTraitors = _cfg.GetCVar(CCVars.SuspicionMinTraitors);
|
||||
PlayersPerTraitor = _cfg.GetCVar(CCVars.SuspicionPlayersPerTraitor);
|
||||
TraitorStartingBalance = _cfg.GetCVar(CCVars.SuspicionStartingBalance);
|
||||
|
||||
if (!force && 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;
|
||||
}
|
||||
|
||||
if (readyPlayers.Count == 0)
|
||||
{
|
||||
_chatManager.DispatchServerAnnouncement("No players readied up! Can't start Suspicion.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var list = new List<IPlayerSession>(readyPlayers);
|
||||
var prefList = new List<IPlayerSession>();
|
||||
|
||||
foreach (var player in list)
|
||||
{
|
||||
if (!ReadyProfiles.ContainsKey(player.UserId) || player.AttachedEntity is not {} attached)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
prefList.Add(player);
|
||||
|
||||
attached.EnsureComponent<SuspicionRoleComponent>();
|
||||
}
|
||||
|
||||
var numTraitors = MathHelper.Clamp(readyPlayers.Count / PlayersPerTraitor,
|
||||
MinTraitors, readyPlayers.Count);
|
||||
|
||||
var traitors = new List<SuspicionTraitorRole>();
|
||||
|
||||
for (var i = 0; i < numTraitors; i++)
|
||||
{
|
||||
IPlayerSession traitor;
|
||||
if(prefList.Count == 0)
|
||||
{
|
||||
if (list.Count == 0)
|
||||
{
|
||||
Logger.InfoS("preset", "Insufficient ready players to fill up with traitors, stopping the selection.");
|
||||
break;
|
||||
}
|
||||
traitor = _random.PickAndTake(list);
|
||||
Logger.InfoS("preset", "Insufficient preferred traitors, picking at random.");
|
||||
}
|
||||
else
|
||||
{
|
||||
traitor = _random.PickAndTake(prefList);
|
||||
list.Remove(traitor);
|
||||
Logger.InfoS("preset", "Selected a preferred traitor.");
|
||||
}
|
||||
var mind = traitor.Data.ContentData()?.Mind;
|
||||
var antagPrototype = _prototypeManager.Index<AntagPrototype>(TraitorID);
|
||||
|
||||
DebugTools.AssertNotNull(mind?.OwnedEntity);
|
||||
|
||||
var traitorRole = new SuspicionTraitorRole(mind!, antagPrototype);
|
||||
mind!.AddRole(traitorRole);
|
||||
traitors.Add(traitorRole);
|
||||
|
||||
// creadth: we need to create uplink for the antag.
|
||||
// PDA should be in place already, so we just need to
|
||||
// initiate uplink account.
|
||||
var uplinkAccount = new UplinkAccount(TraitorStartingBalance, mind.OwnedEntity!);
|
||||
var accounts = EntityManager.EntitySysManager.GetEntitySystem<UplinkAccountsSystem>();
|
||||
accounts.AddNewAccount(uplinkAccount);
|
||||
|
||||
// try to place uplink
|
||||
if (!EntityManager.EntitySysManager.GetEntitySystem<UplinkSystem>()
|
||||
.AddUplink(mind.OwnedEntity!.Value, uplinkAccount))
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var player in list)
|
||||
{
|
||||
var mind = player.Data.ContentData()?.Mind;
|
||||
var antagPrototype = _prototypeManager.Index<AntagPrototype>(InnocentID);
|
||||
|
||||
DebugTools.AssertNotNull(mind);
|
||||
|
||||
mind!.AddRole(new SuspicionInnocentRole(mind, antagPrototype));
|
||||
}
|
||||
|
||||
foreach (var traitor in traitors)
|
||||
{
|
||||
traitor.GreetSuspicion(traitors, _chatManager);
|
||||
}
|
||||
|
||||
EntitySystem.Get<GameTicker>().AddGameRule<RuleSuspicion>();
|
||||
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?";
|
||||
}
|
||||
}
|
||||
@@ -1,244 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Server.GameTicking.Rules;
|
||||
using Content.Server.Objectives.Interfaces;
|
||||
using Content.Server.Players;
|
||||
using Content.Server.Roles;
|
||||
using Content.Server.Traitor;
|
||||
using Content.Server.Traitor.Uplink;
|
||||
using Content.Server.Traitor.Uplink.Account;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Dataset;
|
||||
using Content.Shared.Traitor.Uplink;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.GameTicking.Presets
|
||||
{
|
||||
[GamePreset("traitor")]
|
||||
public class PresetTraitor : GamePreset
|
||||
{
|
||||
[Dependency] private readonly IChatManager _chatManager = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] protected readonly IEntityManager EntityManager = default!;
|
||||
|
||||
public override string ModeTitle => Loc.GetString("traitor-title");
|
||||
|
||||
private int MinPlayers { get; set; }
|
||||
private int PlayersPerTraitor { get; set; }
|
||||
private int MaxTraitors { get; set; }
|
||||
private int CodewordCount { get; set; }
|
||||
private int StartingBalance { get; set; }
|
||||
private float MaxDifficulty { get; set; }
|
||||
private int MaxPicks { get; set; }
|
||||
|
||||
private readonly List<TraitorRole> _traitors = new ();
|
||||
|
||||
public override bool Start(IReadOnlyList<IPlayerSession> readyPlayers, bool force = false)
|
||||
{
|
||||
MinPlayers = _cfg.GetCVar(CCVars.TraitorMinPlayers);
|
||||
PlayersPerTraitor = _cfg.GetCVar(CCVars.TraitorPlayersPerTraitor);
|
||||
MaxTraitors = _cfg.GetCVar(CCVars.TraitorMaxTraitors);
|
||||
CodewordCount = _cfg.GetCVar(CCVars.TraitorCodewordCount);
|
||||
StartingBalance = _cfg.GetCVar(CCVars.TraitorStartingBalance);
|
||||
MaxDifficulty = _cfg.GetCVar(CCVars.TraitorMaxDifficulty);
|
||||
MaxPicks = _cfg.GetCVar(CCVars.TraitorMaxPicks);
|
||||
|
||||
if (!force && readyPlayers.Count < MinPlayers)
|
||||
{
|
||||
_chatManager.DispatchServerAnnouncement(Loc.GetString("traitor-not-enough-ready-players", ("readyPlayersCount", readyPlayers.Count), ("minimumPlayers", MinPlayers)));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (readyPlayers.Count == 0)
|
||||
{
|
||||
_chatManager.DispatchServerAnnouncement(Loc.GetString("traitor-no-one-ready"));
|
||||
return false;
|
||||
}
|
||||
|
||||
var list = new List<IPlayerSession>(readyPlayers).Where(x =>
|
||||
x.Data.ContentData()?.Mind?.AllRoles.All(role => role is not Job {CanBeAntag: false}) ?? false
|
||||
).ToList();
|
||||
|
||||
var prefList = new List<IPlayerSession>();
|
||||
|
||||
foreach (var player in list)
|
||||
{
|
||||
if (!ReadyProfiles.ContainsKey(player.UserId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var profile = ReadyProfiles[player.UserId];
|
||||
if (profile.AntagPreferences.Contains("Traitor"))
|
||||
{
|
||||
prefList.Add(player);
|
||||
}
|
||||
}
|
||||
|
||||
var numTraitors = MathHelper.Clamp(readyPlayers.Count / PlayersPerTraitor,
|
||||
1, MaxTraitors);
|
||||
|
||||
for (var i = 0; i < numTraitors; i++)
|
||||
{
|
||||
IPlayerSession traitor;
|
||||
if(prefList.Count < numTraitors)
|
||||
{
|
||||
if (list.Count == 0)
|
||||
{
|
||||
Logger.InfoS("preset", "Insufficient ready players to fill up with traitors, stopping the selection.");
|
||||
break;
|
||||
}
|
||||
traitor = _random.PickAndTake(list);
|
||||
Logger.InfoS("preset", "Insufficient preferred traitors, picking at random.");
|
||||
}
|
||||
else
|
||||
{
|
||||
traitor = _random.PickAndTake(prefList);
|
||||
list.Remove(traitor);
|
||||
Logger.InfoS("preset", "Selected a preferred traitor.");
|
||||
}
|
||||
var mind = traitor.Data.ContentData()?.Mind;
|
||||
if (mind == null)
|
||||
{
|
||||
Logger.ErrorS("preset", "Failed getting mind for picked traitor.");
|
||||
continue;
|
||||
}
|
||||
|
||||
// creadth: we need to create uplink for the antag.
|
||||
// PDA should be in place already, so we just need to
|
||||
// initiate uplink account.
|
||||
DebugTools.AssertNotNull(mind.OwnedEntity);
|
||||
|
||||
var uplinkAccount = new UplinkAccount(StartingBalance, mind.OwnedEntity!);
|
||||
var accounts = EntityManager.EntitySysManager.GetEntitySystem<UplinkAccountsSystem>();
|
||||
accounts.AddNewAccount(uplinkAccount);
|
||||
|
||||
if (!EntityManager.EntitySysManager.GetEntitySystem<UplinkSystem>()
|
||||
.AddUplink(mind.OwnedEntity!.Value, uplinkAccount))
|
||||
continue;
|
||||
|
||||
var traitorRole = new TraitorRole(mind);
|
||||
mind.AddRole(traitorRole);
|
||||
_traitors.Add(traitorRole);
|
||||
}
|
||||
|
||||
var adjectives = _prototypeManager.Index<DatasetPrototype>("adjectives").Values;
|
||||
var verbs = _prototypeManager.Index<DatasetPrototype>("verbs").Values;
|
||||
|
||||
var codewordPool = adjectives.Concat(verbs).ToList();
|
||||
var finalCodewordCount = Math.Min(CodewordCount, codewordPool.Count);
|
||||
var codewords = new string[finalCodewordCount];
|
||||
for (var i = 0; i < finalCodewordCount; i++)
|
||||
{
|
||||
codewords[i] = _random.PickAndTake(codewordPool);
|
||||
}
|
||||
|
||||
foreach (var traitor in _traitors)
|
||||
{
|
||||
traitor.GreetTraitor(codewords);
|
||||
}
|
||||
|
||||
EntitySystem.Get<GameTicker>().AddGameRule<RuleTraitor>();
|
||||
return true;
|
||||
}
|
||||
|
||||
public override void OnGameStarted()
|
||||
{
|
||||
var objectivesMgr = IoCManager.Resolve<IObjectivesManager>();
|
||||
foreach (var traitor in _traitors)
|
||||
{
|
||||
//give traitors their objectives
|
||||
var difficulty = 0f;
|
||||
for (var pick = 0; pick < MaxPicks && MaxDifficulty > difficulty; pick++)
|
||||
{
|
||||
var objective = objectivesMgr.GetRandomObjective(traitor.Mind);
|
||||
if (objective == null) continue;
|
||||
if (traitor.Mind.TryAddObjective(objective))
|
||||
difficulty += objective.Difficulty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override string GetRoundEndDescription()
|
||||
{
|
||||
var result = Loc.GetString("traitor-round-end-result", ("traitorCount", _traitors.Count));
|
||||
|
||||
foreach (var traitor in _traitors)
|
||||
{
|
||||
var name = traitor.Mind.CharacterName;
|
||||
traitor.Mind.TryGetSession(out var session);
|
||||
var username = session?.Name;
|
||||
|
||||
var objectives = traitor.Mind.AllObjectives.ToArray();
|
||||
if (objectives.Length == 0)
|
||||
{
|
||||
if (username != null)
|
||||
{
|
||||
if (name == null)
|
||||
result += "\n" + Loc.GetString("traitor-user-was-a-traitor", ("user", username));
|
||||
else
|
||||
result += "\n" + Loc.GetString("traitor-user-was-a-traitor-named", ("user", username), ("name", name));
|
||||
}
|
||||
else if (name != null)
|
||||
result += "\n" + Loc.GetString("traitor-was-a-traitor-named", ("name", name));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (username != null)
|
||||
{
|
||||
if (name == null)
|
||||
result += "\n" + Loc.GetString("traitor-user-was-a-traitor-with-objectives", ("user", username));
|
||||
else
|
||||
result += "\n" + Loc.GetString("traitor-user-was-a-traitor-with-objectives-named", ("user", username), ("name", name));
|
||||
}
|
||||
else if (name != null)
|
||||
result += "\n" + Loc.GetString("traitor-was-a-traitor-with-objectives-named", ("name", name));
|
||||
|
||||
foreach (var objectiveGroup in objectives.GroupBy(o => o.Prototype.Issuer))
|
||||
{
|
||||
result += "\n" + Loc.GetString($"preset-traitor-objective-issuer-{objectiveGroup.Key}");
|
||||
|
||||
foreach (var objective in objectiveGroup)
|
||||
{
|
||||
foreach (var condition in objective.Conditions)
|
||||
{
|
||||
var progress = condition.Progress;
|
||||
if (progress > 0.99f)
|
||||
{
|
||||
result += "\n- " + Loc.GetString(
|
||||
"traitor-objective-condition-success",
|
||||
("condition", condition.Title),
|
||||
("markupColor", "green")
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
result += "\n- " + Loc.GetString(
|
||||
"traitor-objective-condition-fail",
|
||||
("condition", condition.Title),
|
||||
("progress", (int) (progress * 100)),
|
||||
("markupColor", "red")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,244 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Server.GameTicking.Rules;
|
||||
using Content.Server.Hands.Components;
|
||||
using Content.Server.Inventory.Components;
|
||||
using Content.Server.Items;
|
||||
using Content.Server.PDA;
|
||||
using Content.Server.Players;
|
||||
using Content.Server.Spawners.Components;
|
||||
using Content.Server.Traitor;
|
||||
using Content.Server.Traitor.Uplink;
|
||||
using Content.Server.Traitor.Uplink.Account;
|
||||
using Content.Server.Traitor.Uplink.Components;
|
||||
using Content.Server.TraitorDeathMatch.Components;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.MobState.Components;
|
||||
using Content.Shared.PDA;
|
||||
using Content.Shared.Traitor.Uplink;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.GameTicking.Presets
|
||||
{
|
||||
[GamePreset("traitordm", "traitordeathmatch")]
|
||||
public sealed class PresetTraitorDeathMatch : GamePreset
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IChatManager _chatManager = default!;
|
||||
[Dependency] private readonly IRobustRandom _robustRandom = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
public string PDAPrototypeName => "CaptainPDA";
|
||||
public string BeltPrototypeName => "ClothingBeltJanitorFilled";
|
||||
public string BackpackPrototypeName => "ClothingBackpackFilled";
|
||||
|
||||
private RuleMaxTimeRestart _restarter = default!;
|
||||
private bool _safeToEndRound = false;
|
||||
|
||||
private Dictionary<UplinkAccount, string> _allOriginalNames = new();
|
||||
|
||||
public override bool Start(IReadOnlyList<IPlayerSession> readyPlayers, bool force = false)
|
||||
{
|
||||
var gameTicker = EntitySystem.Get<GameTicker>();
|
||||
gameTicker.AddGameRule<RuleTraitorDeathMatch>();
|
||||
_restarter = gameTicker.AddGameRule<RuleMaxTimeRestart>();
|
||||
_restarter.RoundMaxTime = TimeSpan.FromMinutes(30);
|
||||
_restarter.RestartTimer();
|
||||
_safeToEndRound = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
public override void OnSpawnPlayerCompleted(IPlayerSession session, EntityUid mob, bool lateJoin)
|
||||
{
|
||||
var startingBalance = _cfg.GetCVar(CCVars.TraitorDeathMatchStartingBalance);
|
||||
|
||||
// Yup, they're a traitor
|
||||
var mind = session.Data.ContentData()?.Mind;
|
||||
if (mind == null)
|
||||
{
|
||||
Logger.ErrorS("preset", "Failed getting mind for TDM player.");
|
||||
return;
|
||||
}
|
||||
|
||||
var traitorRole = new TraitorRole(mind);
|
||||
mind.AddRole(traitorRole);
|
||||
|
||||
// Delete anything that may contain "dangerous" role-specific items.
|
||||
// (This includes the PDA, as everybody gets the captain PDA in this mode for true-all-access reasons.)
|
||||
if (mind.OwnedEntity is {Valid: true} owned && _entityManager.TryGetComponent(owned, out InventoryComponent? inventory))
|
||||
{
|
||||
var victimSlots = new[] {EquipmentSlotDefines.Slots.IDCARD, EquipmentSlotDefines.Slots.BELT, EquipmentSlotDefines.Slots.BACKPACK};
|
||||
foreach (var slot in victimSlots)
|
||||
{
|
||||
if (inventory.TryGetSlotItem(slot, out ItemComponent? vItem))
|
||||
_entityManager.DeleteEntity(vItem.Owner);
|
||||
}
|
||||
|
||||
// Replace their items:
|
||||
|
||||
// pda
|
||||
var newPDA = _entityManager.SpawnEntity(PDAPrototypeName, _entityManager.GetComponent<TransformComponent>(owned).Coordinates);
|
||||
inventory.Equip(EquipmentSlotDefines.Slots.IDCARD, _entityManager.GetComponent<ItemComponent>(newPDA));
|
||||
|
||||
// belt
|
||||
var newTmp = _entityManager.SpawnEntity(BeltPrototypeName, _entityManager.GetComponent<TransformComponent>(owned).Coordinates);
|
||||
inventory.Equip(EquipmentSlotDefines.Slots.BELT, _entityManager.GetComponent<ItemComponent>(newTmp));
|
||||
|
||||
// backpack
|
||||
newTmp = _entityManager.SpawnEntity(BackpackPrototypeName, _entityManager.GetComponent<TransformComponent>(owned).Coordinates);
|
||||
inventory.Equip(EquipmentSlotDefines.Slots.BACKPACK, _entityManager.GetComponent<ItemComponent>(newTmp));
|
||||
|
||||
// Like normal traitors, they need access to a traitor account.
|
||||
var uplinkAccount = new UplinkAccount(startingBalance, owned);
|
||||
var accounts = _entityManager.EntitySysManager.GetEntitySystem<UplinkAccountsSystem>();
|
||||
accounts.AddNewAccount(uplinkAccount);
|
||||
|
||||
_entityManager.EntitySysManager.GetEntitySystem<UplinkSystem>()
|
||||
.AddUplink(owned, uplinkAccount, newPDA);
|
||||
|
||||
_allOriginalNames[uplinkAccount] = _entityManager.GetComponent<MetaDataComponent>(owned).EntityName;
|
||||
|
||||
// The PDA needs to be marked with the correct owner.
|
||||
var pda = _entityManager.GetComponent<PDAComponent>(newPDA);
|
||||
_entityManager.EntitySysManager.GetEntitySystem<PDASystem>()
|
||||
.SetOwner(pda, _entityManager.GetComponent<MetaDataComponent>(owned).EntityName);
|
||||
_entityManager.AddComponent<TraitorDeathMatchReliableOwnerTagComponent>(newPDA).UserId = mind.UserId;
|
||||
}
|
||||
|
||||
// Finally, it would be preferrable if they spawned as far away from other players as reasonably possible.
|
||||
if (mind.OwnedEntity != null && FindAnyIsolatedSpawnLocation(mind, out var bestTarget))
|
||||
{
|
||||
_entityManager.GetComponent<TransformComponent>(mind.OwnedEntity.Value).Coordinates = bestTarget;
|
||||
}
|
||||
else
|
||||
{
|
||||
// The station is too drained of air to safely continue.
|
||||
if (_safeToEndRound)
|
||||
{
|
||||
_chatManager.DispatchServerAnnouncement(Loc.GetString("traitor-death-match-station-is-too-unsafe-announcement"));
|
||||
_restarter.RoundMaxTime = TimeSpan.FromMinutes(1);
|
||||
_restarter.RestartTimer();
|
||||
_safeToEndRound = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// It would be nice if this function were moved to some generic helpers class.
|
||||
private bool FindAnyIsolatedSpawnLocation(Mind.Mind ignoreMe, out EntityCoordinates bestTarget)
|
||||
{
|
||||
// Collate people to avoid...
|
||||
var existingPlayerPoints = new List<EntityCoordinates>();
|
||||
foreach (var player in _playerManager.ServerSessions)
|
||||
{
|
||||
var avoidMeMind = player.Data.ContentData()?.Mind;
|
||||
if ((avoidMeMind == null) || (avoidMeMind == ignoreMe))
|
||||
continue;
|
||||
var avoidMeEntity = avoidMeMind.OwnedEntity;
|
||||
if (avoidMeEntity == null)
|
||||
continue;
|
||||
if (_entityManager.TryGetComponent(avoidMeEntity.Value, out MobStateComponent? mobState))
|
||||
{
|
||||
// Does have mob state component; if critical or dead, they don't really matter for spawn checks
|
||||
if (mobState.IsCritical() || mobState.IsDead())
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Doesn't have mob state component. Assume something interesting is going on and don't count this as someone to avoid.
|
||||
continue;
|
||||
}
|
||||
existingPlayerPoints.Add(_entityManager.GetComponent<TransformComponent>(avoidMeEntity.Value).Coordinates);
|
||||
}
|
||||
|
||||
// Iterate over each possible spawn point, comparing to the existing player points.
|
||||
// On failure, the returned target is the location that we're already at.
|
||||
var bestTargetDistanceFromNearest = -1.0f;
|
||||
// Need the random shuffle or it stuffs the first person into Atmospherics pretty reliably
|
||||
var ents = _entityManager.EntityQuery<SpawnPointComponent>().Select(x => x.Owner).ToList();
|
||||
_robustRandom.Shuffle(ents);
|
||||
var foundATarget = false;
|
||||
bestTarget = EntityCoordinates.Invalid;
|
||||
var atmosphereSystem = EntitySystem.Get<AtmosphereSystem>();
|
||||
foreach (var entity in ents)
|
||||
{
|
||||
if (!atmosphereSystem.IsTileMixtureProbablySafe(_entityManager.GetComponent<TransformComponent>(entity).Coordinates))
|
||||
continue;
|
||||
|
||||
var distanceFromNearest = float.PositiveInfinity;
|
||||
foreach (var existing in existingPlayerPoints)
|
||||
{
|
||||
if (_entityManager.GetComponent<TransformComponent>(entity).Coordinates.TryDistance(_entityManager, existing, out var dist))
|
||||
distanceFromNearest = Math.Min(distanceFromNearest, dist);
|
||||
}
|
||||
if (bestTargetDistanceFromNearest < distanceFromNearest)
|
||||
{
|
||||
bestTarget = _entityManager.GetComponent<TransformComponent>(entity).Coordinates;
|
||||
bestTargetDistanceFromNearest = distanceFromNearest;
|
||||
foundATarget = true;
|
||||
}
|
||||
}
|
||||
return foundATarget;
|
||||
}
|
||||
|
||||
public override bool OnGhostAttempt(Mind.Mind mind, bool canReturnGlobal)
|
||||
{
|
||||
if (mind.OwnedEntity is {Valid: true} entity && _entityManager.TryGetComponent(entity, out MobStateComponent? mobState))
|
||||
{
|
||||
if (mobState.IsCritical())
|
||||
{
|
||||
// TODO BODY SYSTEM KILL
|
||||
var damage = new DamageSpecifier(_prototypeManager.Index<DamageTypePrototype>("Asphyxiation"), 100);
|
||||
EntitySystem.Get<DamageableSystem>().TryChangeDamage(entity, damage, true);
|
||||
}
|
||||
else if (!mobState.IsDead())
|
||||
{
|
||||
if (_entityManager.HasComponent<HandsComponent>(entity))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
var session = mind.Session;
|
||||
if (session == null)
|
||||
return false;
|
||||
EntitySystem.Get<GameTicker>().Respawn(session);
|
||||
return true;
|
||||
}
|
||||
|
||||
public override string GetRoundEndDescription()
|
||||
{
|
||||
var lines = new List<string>();
|
||||
lines.Add(Loc.GetString("traitor-death-match-end-round-description-first-line"));
|
||||
foreach (var uplink in _entityManager.EntityQuery<UplinkComponent>(true))
|
||||
{
|
||||
var uplinkAcc = uplink.UplinkAccount;
|
||||
if (uplinkAcc != null && _allOriginalNames.ContainsKey(uplinkAcc))
|
||||
{
|
||||
lines.Add(Loc.GetString("traitor-death-match-end-round-description-entry",
|
||||
("originalName", _allOriginalNames[uplinkAcc]),
|
||||
("tcBalance", uplinkAcc.Balance)));
|
||||
}
|
||||
}
|
||||
return string.Join('\n', lines);
|
||||
}
|
||||
|
||||
public override string ModeTitle => Loc.GetString("traitor-death-match-title");
|
||||
public override string Description => Loc.GetString("traitor-death-match-description");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user