Revolutionaries (#18477)

Co-authored-by: coolmankid12345 <coolmankid12345@users.noreply.github.com>
Co-authored-by: EmoGarbage404 <retron404@gmail.com>
This commit is contained in:
coolmankid12345
2023-10-04 21:47:33 -04:00
committed by GitHub
parent 2c69dd2df9
commit 9df5ded9ad
51 changed files with 1246 additions and 38 deletions

View File

@@ -1,3 +1,4 @@
using Content.Server.GameTicking;
using Content.Server.GameTicking.Rules;
using Content.Server.Zombies;
using Content.Shared.Administration;
@@ -8,6 +9,8 @@ using Content.Shared.Mind.Components;
using Content.Shared.Verbs;
using Robust.Server.GameObjects;
using Robust.Shared.Utility;
using Content.Server.GameTicking.Rules.Components;
using System.Linq;
namespace Content.Server.Administration.Systems;
@@ -17,7 +20,9 @@ public sealed partial class AdminVerbSystem
[Dependency] private readonly TraitorRuleSystem _traitorRule = default!;
[Dependency] private readonly NukeopsRuleSystem _nukeopsRule = default!;
[Dependency] private readonly PiratesRuleSystem _piratesRule = default!;
[Dependency] private readonly RevolutionaryRuleSystem _revolutionaryRule = default!;
[Dependency] private readonly SharedMindSystem _minds = default!;
[Dependency] private readonly GameTicker _gameTicker = default!;
// All antag verbs have names so invokeverb works.
private void AddAntagVerbs(GetVerbsEvent<Verb> args)
@@ -100,5 +105,22 @@ public sealed partial class AdminVerbSystem
Message = Loc.GetString("admin-verb-make-pirate"),
};
args.Verbs.Add(pirate);
//todo come here at some point dear lort.
Verb headRev = new()
{
Text = Loc.GetString("admin-verb-text-make-head-rev"),
Category = VerbCategory.Antag,
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/Misc/job_icons.rsi/HeadRevolutionary.png")),
Act = () =>
{
if (!_minds.TryGetMind(args.Target, out var mindId, out var mind))
return;
_revolutionaryRule.OnHeadRevAdmin(mindId, mind);
},
Impact = LogImpact.High,
Message = Loc.GetString("admin-verb-make-head-rev"),
};
args.Verbs.Add(headRev);
}
}

View File

@@ -0,0 +1,237 @@
using Content.Server.GameTicking.Rules;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Roles.Jobs;
using Content.Server.Preferences.Managers;
using Content.Shared.Humanoid;
using Content.Shared.Preferences;
using Robust.Server.Player;
using System.Linq;
using Content.Server.Mind;
using Robust.Shared.Random;
using Robust.Shared.Map;
using System.Numerics;
using Content.Shared.Inventory;
using Content.Server.Storage.EntitySystems;
using Robust.Shared.Audio;
using Robust.Server.GameObjects;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking;
using Robust.Shared.Containers;
using Content.Shared.Mobs.Components;
using Content.Server.Station.Systems;
using Content.Server.Shuttles.Systems;
using Content.Shared.Mobs;
using Robust.Server.Containers;
using Robust.Shared.Prototypes;
namespace Content.Server.Antag;
public sealed class AntagSelectionSystem : GameRuleSystem<GameRuleComponent>
{
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly IServerPreferencesManager _prefs = default!;
[Dependency] private readonly IPlayerManager _playerSystem = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly AudioSystem _audioSystem = default!;
[Dependency] private readonly ContainerSystem _containerSystem = default!;
[Dependency] private readonly JobSystem _jobs = default!;
[Dependency] private readonly MindSystem _mindSystem = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly StorageSystem _storageSystem = default!;
[Dependency] private readonly StationSystem _stationSystem = default!;
[Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
/// <summary>
/// Attempts to start the game rule by checking if there are enough players in lobby and readied.
/// </summary>
/// <param name="ev">The roundstart attempt event</param>
/// <param name="uid">The entity the gamerule you are using is on</param>
/// <param name="minPlayers">The minimum amount of players needed for you gamerule to start.</param>
/// <param name="gameRule">The gamerule component.</param>
public void AttemptStartGameRule(RoundStartAttemptEvent ev, EntityUid uid, int minPlayers, GameRuleComponent gameRule)
{
if (GameTicker.IsGameRuleAdded(uid, gameRule))
{
if (!ev.Forced && ev.Players.Length < minPlayers)
{
_chatManager.SendAdminAnnouncement(Loc.GetString("rev-not-enough-ready-players",
("readyPlayersCount", ev.Players.Length),
("minimumPlayers", minPlayers)));
ev.Cancel();
}
else if (ev.Players.Length == 0)
{
_chatManager.DispatchServerAnnouncement(Loc.GetString("rev-no-one-ready"));
ev.Cancel();
}
}
}
/// <summary>
/// Will check which players are eligible to be chosen for antagonist and give them the given antag.
/// </summary>
/// <param name="antagPrototype">The antag prototype from your rule component.</param>
/// <param name="maxAntags">How many antags can be present in any given round.</param>
/// <param name="antagsPerPlayer">How many players you need to spawn an additional antag.</param>
/// <param name="antagSound">The intro sound that plays when the antag is chosen.</param>
/// <param name="antagGreeting">The antag message you want shown when the antag is chosen.</param>
/// <param name="greetingColor">The color of the message for the antag greeting in hex.</param>
/// <param name="chosen">A list of all the antags chosen in case you need to add stuff after.</param>
/// <param name="includeHeads">Whether or not heads can be chosen as antags for this gamemode.</param>
public void EligiblePlayers(string antagPrototype,
int maxAntags,
int antagsPerPlayer,
SoundSpecifier? antagSound,
string antagGreeting,
string greetingColor,
out List<EntityUid> chosen,
bool includeHeads = false)
{
var allPlayers = _playerSystem.ServerSessions.ToList();
var playerList = new List<IPlayerSession>();
var prefList = new List<IPlayerSession>();
chosen = new List<EntityUid>();
foreach (var player in allPlayers)
{
if (includeHeads == false)
{
if (!_jobs.CanBeAntag(player))
continue;
}
if (player.AttachedEntity == null || HasComp<HumanoidAppearanceComponent>(player.AttachedEntity))
playerList.Add(player);
else
continue;
var pref = (HumanoidCharacterProfile) _prefs.GetPreferences(player.UserId).SelectedCharacter;
if (pref.AntagPreferences.Contains(antagPrototype))
prefList.Add(player);
}
if (playerList.Count == 0)
return;
var antags = Math.Clamp(allPlayers.Count / antagsPerPlayer, 1, maxAntags);
for (var antag = 0; antag < antags; antag++)
{
IPlayerSession chosenPlayer;
if (prefList.Count == 0)
{
if (playerList.Count == 0)
{
break;
}
chosenPlayer = _random.PickAndTake(playerList);
}
else
{
chosenPlayer = _random.PickAndTake(prefList);
playerList.Remove(chosenPlayer);
}
if (!_mindSystem.TryGetMind(chosenPlayer, out _, out var mind) ||
mind.OwnedEntity is not { } ownedEntity)
{
continue;
}
chosen.Add(ownedEntity);
_audioSystem.PlayGlobal(antagSound, ownedEntity);
if (mind.Session != null)
{
var message = Loc.GetString(antagGreeting);
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", message));
_chatManager.ChatMessageToOne(Shared.Chat.ChatChannel.Server, message, wrappedMessage, default, false, mind.Session.ConnectedClient, Color.FromHex(greetingColor));
}
}
}
/// <summary>
/// Will take a group of entities and check if they are all alive or dead
/// </summary>
/// <param name="list">The list of the entities</param>
/// <param name="checkOffStation">Bool for if you want to check if someone is in space and consider them dead. (Won't check when emergency shuttle arrives just in case)</param>
/// <returns></returns>
public bool IsGroupDead(List<EntityUid> list, bool checkOffStation)
{
var dead = 0;
foreach (var entity in list)
{
if (TryComp<MobStateComponent>(entity, out var state))
{
if (state.CurrentState == MobState.Dead || state.CurrentState == MobState.Invalid)
{
dead++;
}
else if (checkOffStation && _stationSystem.GetOwningStation(entity) == null && !_emergencyShuttle.EmergencyShuttleArrived)
{
dead++;
}
}
//If they don't have the MobStateComponent they might as well be dead.
else
{
dead++;
}
}
return dead == list.Count || list.Count == 0;
}
/// <summary>
/// Will attempt to spawn an item inside of a persons bag and then pockets.
/// </summary>
/// <param name="antag">The entity that you want to spawn an item on</param>
/// <param name="items">A list of prototype IDs that you want to spawn in the bag.</param>
public void GiveAntagBagGear(EntityUid antag, List<EntProtoId> items)
{
foreach (var item in items)
{
GiveAntagBagGear(antag, item);
}
}
/// <summary>
/// Will attempt to spawn an item inside of a persons bag and then pockets.
/// </summary>
/// <param name="antag">The entity that you want to spawn an item on</param>
/// <param name="item">The prototype ID that you want to spawn in the bag.</param>
public void GiveAntagBagGear(EntityUid antag, string item)
{
var itemToSpawn = Spawn(item, new EntityCoordinates(antag, Vector2.Zero));
if (!_inventory.TryGetSlotContainer(antag, "back", out var backSlot, out _))
return;
var bag = backSlot.ContainedEntity;
if (bag != null && HasComp<ContainerManagerComponent>(bag) && _storageSystem.CanInsert(bag.Value, itemToSpawn, out _))
{
_storageSystem.Insert(bag.Value, itemToSpawn, out _);
}
else if (_inventory.TryGetSlotContainer(antag, "jumpsuit", out var jumpsuit, out _) && jumpsuit.ContainedEntity != null)
{
if (_inventory.TryGetSlotContainer(antag, "pocket1", out var pocket1Slot, out _))
{
if (pocket1Slot.ContainedEntity == null)
{
if (_containerSystem.CanInsert(itemToSpawn, pocket1Slot))
{
pocket1Slot.Insert(itemToSpawn);
}
}
else if (_inventory.TryGetSlotContainer(antag, "pocket2", out var pocket2Slot, out _))
{
if (pocket2Slot.ContainedEntity == null)
{
if (_containerSystem.CanInsert(itemToSpawn, pocket2Slot))
{
pocket2Slot.Insert(itemToSpawn);
}
}
}
}
}
}
}

View File

@@ -5,7 +5,6 @@ using Content.Server.Popups;
using Content.Server.Stunnable;
using Content.Shared.Charges.Components;
using Content.Shared.Charges.Systems;
using Content.Shared.Damage;
using Content.Shared.Eye.Blinding.Components;
using Content.Shared.Flash;
using Content.Shared.IdentityManagement;
@@ -13,7 +12,6 @@ using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Inventory;
using Content.Shared.Physics;
using Content.Shared.Popups;
using Content.Shared.Tag;
using Content.Shared.Traits.Assorted;
using Content.Shared.Weapons.Melee.Events;
@@ -41,11 +39,11 @@ namespace Content.Server.Flash
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<FlashComponent, MeleeHitEvent>(OnFlashMeleeHit);
// ran before toggling light for extra-bright lantern
SubscribeLocalEvent<FlashComponent, UseInHandEvent>(OnFlashUseInHand, before: new []{ typeof(HandheldLightSystem) });
SubscribeLocalEvent<InventoryComponent, FlashAttemptEvent>(OnInventoryFlashAttempt);
SubscribeLocalEvent<FlashImmunityComponent, FlashAttemptEvent>(OnFlashImmunityFlashAttempt);
SubscribeLocalEvent<PermanentBlindnessComponent, FlashAttemptEvent>(OnPermanentBlindnessFlashAttempt);
SubscribeLocalEvent<TemporaryBlindnessComponent, FlashAttemptEvent>(OnTemporaryBlindnessFlashAttempt);
@@ -63,7 +61,7 @@ namespace Content.Server.Flash
args.Handled = true;
foreach (var e in args.HitEntities)
{
Flash(e, args.User, uid, comp.FlashDuration, comp.SlowTo);
Flash(e, args.User, uid, comp.FlashDuration, comp.SlowTo, melee: true);
}
}
@@ -106,9 +104,17 @@ namespace Content.Server.Flash
return true;
}
public void Flash(EntityUid target, EntityUid? user, EntityUid? used, float flashDuration, float slowTo, bool displayPopup = true, FlashableComponent? flashable = null)
public void Flash(EntityUid target,
EntityUid? user,
EntityUid? used,
float flashDuration,
float slowTo,
bool displayPopup = true,
FlashableComponent? flashable = null,
bool melee = false)
{
if (!Resolve(target, ref flashable, false)) return;
if (!Resolve(target, ref flashable, false))
return;
var attempt = new FlashAttemptEvent(target, user, used);
RaiseLocalEvent(target, attempt, true);
@@ -116,18 +122,28 @@ namespace Content.Server.Flash
if (attempt.Cancelled)
return;
if (melee)
{
var ev = new AfterFlashedEvent(target, user, used);
if (user != null)
RaiseLocalEvent(user.Value, ref ev);
if (used != null)
RaiseLocalEvent(used.Value, ref ev);
}
flashable.LastFlash = _timing.CurTime;
flashable.Duration = flashDuration / 1000f; // TODO: Make this sane...
Dirty(flashable);
Dirty(target, flashable);
_stun.TrySlowdown(target, TimeSpan.FromSeconds(flashDuration/1000f), true,
slowTo, slowTo);
if (displayPopup && user != null && target != user && EntityManager.EntityExists(user.Value))
if (displayPopup && user != null && target != user && Exists(user.Value))
{
user.Value.PopupMessage(target, Loc.GetString("flash-component-user-blinds-you",
("user", Identity.Entity(user.Value, EntityManager))));
_popup.PopupEntity(Loc.GetString("flash-component-user-blinds-you",
("user", Identity.Entity(user.Value, EntityManager))), target, target);
}
}
public void FlashArea(EntityUid source, EntityUid? user, float range, float duration, float slowTo = 0.8f, bool displayPopup = false, SoundSpecifier? sound = null)
@@ -201,4 +217,24 @@ namespace Content.Server.Flash
Used = used;
}
}
/// <summary>
/// Called after a flash is used via melee on another person to check for rev conversion.
/// Raised on the user of the flash, the target hit by the flash, and the flash used.
/// </summary>
[ByRefEvent]
public readonly struct AfterFlashedEvent
{
public readonly EntityUid Target;
public readonly EntityUid? User;
public readonly EntityUid? Used;
public AfterFlashedEvent(EntityUid target, EntityUid? user, EntityUid? used)
{
Target = target;
User = user;
Used = used;
}
}
}

View File

@@ -0,0 +1,74 @@
using Content.Shared.Roles;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.GameTicking.Rules.Components;
/// <summary>
/// Component for the RevolutionaryRuleSystem that stores info about winning/losing, player counts required for starting, as well as prototypes for Revolutionaries and their gear.
/// </summary>
[RegisterComponent, Access(typeof(RevolutionaryRuleSystem))]
public sealed partial class RevolutionaryRuleComponent : Component
{
/// <summary>
/// When the round will if all the command are dead (Incase they are in space)
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan CommandCheck;
/// <summary>
/// The amount of time between each check for command check.
/// </summary>
[DataField]
public TimeSpan TimerWait = TimeSpan.FromSeconds(20);
/// <summary>
/// Stores players minds
/// </summary>
[DataField]
public Dictionary<string, EntityUid> HeadRevs = new();
[DataField]
public ProtoId<AntagPrototype> RevPrototypeId = "Rev";
/// <summary>
/// Sound that plays when you are chosen as Rev. (Placeholder until I find something cool I guess)
/// </summary>
[DataField]
public SoundSpecifier HeadRevStartSound = new SoundPathSpecifier("/Audio/Ambience/Antag/traitor_start.ogg");
/// <summary>
/// Min players needed for Revolutionary gamemode to start.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public int MinPlayers = 15;
/// <summary>
/// Max Head Revs allowed during selection.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public int MaxHeadRevs = 3;
/// <summary>
/// The amount of Head Revs that will spawn per this amount of players.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public int PlayersPerHeadRev = 15;
/// <summary>
/// The gear head revolutionaries are given on spawn.
/// </summary>
[DataField]
public List<EntProtoId> StartingGear = new()
{
"Flash",
"ClothingEyesGlassesSunglasses"
};
/// <summary>
/// The time it takes after the last head is killed for the shuttle to arrive.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public TimeSpan ShuttleCallTime = TimeSpan.FromMinutes(5);
}

View File

@@ -0,0 +1,311 @@
using System.Linq;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Mind;
using Content.Server.NPC.Systems;
using Content.Server.Roles;
using Content.Shared.Humanoid;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Revolutionary.Components;
using Content.Shared.Roles;
using Content.Shared.Stunnable;
using Robust.Shared.Timing;
using Content.Server.Popups;
using Content.Server.Revolutionary.Components;
using Content.Shared.IdentityManagement;
using Content.Server.Flash;
using Content.Shared.Mindshield.Components;
using Content.Server.Administration.Logs;
using Content.Shared.Database;
using Content.Server.Antag;
using Content.Server.NPC.Components;
using Content.Server.RoundEnd;
using Content.Shared.Chat;
using Content.Shared.Mind;
using Content.Shared.Mobs.Systems;
using Content.Shared.Zombies;
namespace Content.Server.GameTicking.Rules;
/// <summary>
/// Where all the main stuff for Revolutionaries happens (Assigning Head Revs, Command on station, and checking for the game to end.)
/// </summary>
public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleComponent>
{
[Dependency] private readonly IAdminLogManager _adminLogManager = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly AntagSelectionSystem _antagSelection = default!;
[Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly NpcFactionSystem _npcFaction = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly RoleSystem _role = default!;
[Dependency] private readonly SharedStunSystem _stun = default!;
[Dependency] private readonly RoundEndSystem _roundEnd = default!;
[ValidatePrototypeId<NpcFactionPrototype>]
public const string RevolutionaryNpcFaction = "Revolutionary";
[ValidatePrototypeId<AntagPrototype>]
public const string RevolutionaryAntagRole = "Rev";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RoundStartAttemptEvent>(OnStartAttempt);
SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(OnPlayerJobAssigned);
SubscribeLocalEvent<CommandStaffComponent, MobStateChangedEvent>(OnCommandMobStateChanged);
SubscribeLocalEvent<HeadRevolutionaryComponent, MobStateChangedEvent>(OnHeadRevMobStateChanged);
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndText);
SubscribeLocalEvent<HeadRevolutionaryComponent, AfterFlashedEvent>(OnPostFlash);
}
protected override void Started(EntityUid uid, RevolutionaryRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
{
base.Started(uid, component, gameRule, args);
component.CommandCheck = _timing.CurTime + component.TimerWait;
}
/// <summary>
/// Checks if the round should end and also checks who has a mindshield.
/// </summary>
protected override void ActiveTick(EntityUid uid, RevolutionaryRuleComponent component, GameRuleComponent gameRule, float frameTime)
{
base.ActiveTick(uid, component, gameRule, frameTime);
if (component.CommandCheck <= _timing.CurTime)
{
component.CommandCheck = _timing.CurTime + component.TimerWait;
if (CheckCommandLose())
{
_roundEnd.DoRoundEndBehavior(RoundEndBehavior.ShuttleCall, component.ShuttleCallTime);
GameTicker.EndGameRule(uid, gameRule);
}
}
}
private void OnRoundEndText(RoundEndTextAppendEvent ev)
{
var revsLost = CheckRevsLose();
var commandLost = CheckCommandLose();
var query = AllEntityQuery<RevolutionaryRuleComponent>();
while (query.MoveNext(out var headrev))
{
// This is (revsLost, commandsLost) concatted together
// (moony wrote this comment idk what it means)
var index = (commandLost ? 1 : 0) | (revsLost ? 2 : 0);
ev.AddLine(Loc.GetString(Outcomes[index]));
ev.AddLine(Loc.GetString("head-rev-initial-count", ("initialCount", headrev.HeadRevs.Count)));
foreach (var player in headrev.HeadRevs)
{
_mind.TryGetSession(player.Value, out var session);
var username = session?.Name;
if (username != null)
{
ev.AddLine(Loc.GetString("head-rev-initial",
("name", player.Key),
("username", username)));
}
else
{
ev.AddLine(Loc.GetString("head-rev-initial",
("name", player.Key)));
}
}
break;
}
}
private void OnStartAttempt(RoundStartAttemptEvent ev)
{
var query = AllEntityQuery<RevolutionaryRuleComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out var comp, out var gameRule))
{
_antagSelection.AttemptStartGameRule(ev, uid, comp.MinPlayers, gameRule);
}
}
private void OnPlayerJobAssigned(RulePlayerJobsAssignedEvent ev)
{
var query = QueryActiveRules();
while (query.MoveNext(out _, out var comp, out _))
{
_antagSelection.EligiblePlayers(comp.RevPrototypeId, comp.MaxHeadRevs, comp.PlayersPerHeadRev, comp.HeadRevStartSound,
"head-rev-role-greeting", "#5e9cff", out var chosen);
GiveHeadRev(chosen, comp.RevPrototypeId, comp);
}
}
private void GiveHeadRev(List<EntityUid> chosen, string antagProto, RevolutionaryRuleComponent comp)
{
foreach (var headRev in chosen)
{
RemComp<CommandStaffComponent>(headRev);
var inCharacterName = MetaData(headRev).EntityName;
if (_mind.TryGetMind(headRev, out var mindId, out var mind))
{
if (!_role.MindHasRole<RevolutionaryRoleComponent>(mindId))
{
_role.MindAddRole(mindId, new RevolutionaryRoleComponent { PrototypeId = antagProto });
}
if (mind.Session != null)
{
comp.HeadRevs.Add(inCharacterName, mindId);
}
}
_antagSelection.GiveAntagBagGear(headRev, comp.StartingGear);
EnsureComp<RevolutionaryComponent>(headRev);
EnsureComp<HeadRevolutionaryComponent>(headRev);
}
}
/// <summary>
/// Called when a Head Rev uses a flash in melee to convert somebody else.
/// </summary>
public void OnPostFlash(EntityUid uid, HeadRevolutionaryComponent comp, ref AfterFlashedEvent ev)
{
TryComp<AlwaysRevolutionaryConvertibleComponent>(ev.Target, out var alwaysConvertibleComp);
var alwaysConvertible = alwaysConvertibleComp != null;
if (!_mind.TryGetMind(ev.Target, out var mindId, out var mind) && !alwaysConvertible)
return;
if (HasComp<RevolutionaryComponent>(ev.Target) ||
HasComp<MindShieldComponent>(ev.Target) ||
!HasComp<HumanoidAppearanceComponent>(ev.Target) &&
!alwaysConvertible ||
!_mobState.IsAlive(ev.Target) ||
HasComp<ZombieComponent>(ev.Target))
{
return;
}
_npcFaction.AddFaction(ev.Target, RevolutionaryNpcFaction);
EnsureComp<RevolutionaryComponent>(ev.Target);
_stun.TryParalyze(ev.Target, comp.StunTime, true);
if (ev.User != null)
{
_adminLogManager.Add(LogType.Mind, LogImpact.Medium, $"{ToPrettyString(ev.User.Value)} converted {ToPrettyString(ev.Target)} into a Revolutionary");
}
if (mindId == default || !_role.MindHasRole<RevolutionaryRoleComponent>(mindId))
{
_role.MindAddRole(mindId, new RevolutionaryRoleComponent { PrototypeId = RevolutionaryAntagRole });
}
if (mind?.Session != null)
{
var message = Loc.GetString("rev-role-greeting");
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", message));
_chatManager.ChatMessageToOne(ChatChannel.Server, message, wrappedMessage, default, false, mind.Session.ConnectedClient, Color.Red);
}
}
public void OnHeadRevAdmin(EntityUid mindId, MindComponent? mind = null)
{
if (!Resolve(mindId, ref mind))
return;
var revRule = EntityQuery<RevolutionaryRuleComponent>().FirstOrDefault();
if (revRule == null)
{
GameTicker.StartGameRule("Revolutionary", out var ruleEnt);
revRule = Comp<RevolutionaryRuleComponent>(ruleEnt);
}
if (!HasComp<HeadRevolutionaryComponent>(mind.OwnedEntity))
{
if (mind.OwnedEntity != null)
{
var player = new List<EntityUid>
{
mind.OwnedEntity.Value
};
GiveHeadRev(player, RevolutionaryAntagRole, revRule);
}
if (mind.Session != null)
{
var message = Loc.GetString("head-rev-role-greeting");
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", message));
_chatManager.ChatMessageToOne(ChatChannel.Server, message, wrappedMessage, default, false, mind.Session.ConnectedClient, Color.FromHex("#5e9cff"));
}
}
}
private void OnCommandMobStateChanged(EntityUid uid, CommandStaffComponent comp, MobStateChangedEvent ev)
{
if (ev.NewMobState == MobState.Dead || ev.NewMobState == MobState.Invalid)
CheckCommandLose();
}
/// <summary>
/// Checks if all of command is dead and if so will remove all sec and command jobs if there were any left.
/// </summary>
private bool CheckCommandLose()
{
var commandList = new List<EntityUid>();
var heads = AllEntityQuery<CommandStaffComponent>();
while (heads.MoveNext(out var id, out _))
{
commandList.Add(id);
}
return _antagSelection.IsGroupDead(commandList, true);
}
private void OnHeadRevMobStateChanged(EntityUid uid, HeadRevolutionaryComponent comp, MobStateChangedEvent ev)
{
if (ev.NewMobState == MobState.Dead || ev.NewMobState == MobState.Invalid)
CheckRevsLose();
}
/// <summary>
/// Checks if all the Head Revs are dead and if so will deconvert all regular revs.
/// </summary>
private bool CheckRevsLose()
{
var stunTime = TimeSpan.FromSeconds(4);
var headRevList = new List<EntityUid>();
var headRevs = AllEntityQuery<HeadRevolutionaryComponent, MobStateComponent>();
while (headRevs.MoveNext(out var uid, out _, out _))
{
headRevList.Add(uid);
}
// If no Head Revs are alive all normal Revs will lose their Rev status and rejoin Nanotrasen
if (_antagSelection.IsGroupDead(headRevList, false))
{
var rev = AllEntityQuery<RevolutionaryComponent>();
while (rev.MoveNext(out var uid, out _))
{
if (!HasComp<HeadRevolutionaryComponent>(uid))
{
_npcFaction.RemoveFaction(uid, RevolutionaryNpcFaction);
_stun.TryParalyze(uid, stunTime, true);
RemCompDeferred<RevolutionaryComponent>(uid);
_popup.PopupEntity(Loc.GetString("rev-break-control", ("name", Identity.Entity(uid, EntityManager))), uid);
_adminLogManager.Add(LogType.Mind, LogImpact.Medium, $"{ToPrettyString(uid)} was deconverted due to all Head Revolutionaries dying.");
}
}
return true;
}
return false;
}
private static readonly string[] Outcomes =
{
// revs survived and heads survived... how
"rev-reverse-stalemate",
// revs won and heads died
"rev-won",
// revs lost and heads survived
"rev-lost",
// revs lost and heads died
"rev-stalemate"
};
}

View File

@@ -0,0 +1,63 @@
using Content.Shared.Mindshield.Components;
using Content.Shared.Revolutionary.Components;
using Content.Server.Popups;
using Content.Shared.Database;
using Content.Server.Administration.Logs;
using Content.Server.Mind;
using Content.Shared.Implants;
using Content.Shared.Tag;
using Content.Server.Roles;
using Content.Shared.Implants.Components;
namespace Content.Server.Mindshield;
/// <summary>
/// System used for checking if the implanted is a Rev or Head Rev.
/// </summary>
public sealed class MindShieldSystem : EntitySystem
{
[Dependency] private readonly IAdminLogManager _adminLogManager = default!;
[Dependency] private readonly RoleSystem _roleSystem = default!;
[Dependency] private readonly MindSystem _mindSystem = default!;
[Dependency] private readonly TagSystem _tag = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[ValidatePrototypeId<TagPrototype>]
public const string MindShieldTag = "MindShield";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SubdermalImplantComponent, ImplantImplantedEvent>(ImplantCheck);
}
/// <summary>
/// Checks if the implant was a mindshield or not
/// </summary>
public void ImplantCheck(EntityUid uid, SubdermalImplantComponent comp, ref ImplantImplantedEvent ev)
{
if (_tag.HasTag(ev.Implant, MindShieldTag) && ev.Implanted != null)
{
EnsureComp<MindShieldComponent>(ev.Implanted.Value);
MindShieldRemovalCheck(ev.Implanted, ev.Implant);
}
}
/// <summary>
/// Checks if the implanted person was a Rev or Head Rev and remove role or destroy mindshield respectively.
/// </summary>
public void MindShieldRemovalCheck(EntityUid? implanted, EntityUid implant)
{
if (HasComp<RevolutionaryComponent>(implanted) && !HasComp<HeadRevolutionaryComponent>(implanted))
{
_mindSystem.TryGetMind(implanted.Value, out var mindId, out _);
_adminLogManager.Add(LogType.Mind, LogImpact.Medium, $"{ToPrettyString(implanted.Value)} was deconverted due to being implanted with a Mindshield.");
_roleSystem.MindTryRemoveRole<RevolutionaryRoleComponent>(mindId);
}
else if (HasComp<RevolutionaryComponent>(implanted))
{
_popupSystem.PopupEntity(Loc.GetString("head-rev-break-mindshield"), implanted.Value);
QueueDel(implant);
}
}
}

View File

@@ -0,0 +1,12 @@
using Content.Server.GameTicking.Rules;
namespace Content.Server.Revolutionary.Components;
/// <summary>
/// Given to heads at round start for Revs. Used for tracking if heads died or not.
/// </summary>
[RegisterComponent, Access(typeof(RevolutionaryRuleSystem))]
public sealed partial class CommandStaffComponent : Component
{
}

View File

@@ -0,0 +1,12 @@
using Content.Shared.Roles;
namespace Content.Server.Roles;
/// <summary>
/// Added to mind entities to tag that they are a Revolutionary.
/// </summary>
[RegisterComponent]
public sealed partial class RevolutionaryRoleComponent : AntagonistRoleComponent
{
}

View File

@@ -224,7 +224,19 @@ namespace Content.Server.RoundEnd
Timer.Spawn(countdownTime.Value, AfterEndRoundRestart, _countdownTokenSource.Token);
}
public void DoRoundEndBehavior(RoundEndBehavior behavior, TimeSpan time, string sender, string textCall, string textAnnounce)
/// <summary>
/// Starts a behavior to end the round
/// </summary>
/// <param name="behavior">The way in which the round will end</param>
/// <param name="time"></param>
/// <param name="sender"></param>
/// <param name="textCall"></param>
/// <param name="textAnnounce"></param>
public void DoRoundEndBehavior(RoundEndBehavior behavior,
TimeSpan time,
string sender = "comms-console-announcement-title-centcom",
string textCall = "round-end-system-shuttle-called-announcement",
string textAnnounce = "round-end-system-shuttle-already-called-announcement")
{
switch (behavior)
{