Zombie Mode 𝓡𝓮𝓭𝓾𝔁 (#18199)

* zombie mode redux

* the great zombie changes

* fix this

* 65 down to 50

* empty

* Changes to address stalling

* make zombie nukies no longer nukies

* actually work
This commit is contained in:
Nemanja
2023-07-25 17:31:35 -04:00
committed by GitHub
parent 763156f6ec
commit d55cd23b0a
20 changed files with 604 additions and 493 deletions

View File

@@ -1,8 +1,46 @@
using Content.Shared.Zombies;
using System.Linq;
using Content.Shared.Humanoid;
using Content.Shared.StatusIcon;
using Content.Shared.StatusIcon.Components;
using Content.Shared.Zombies;
using Robust.Client.GameObjects;
using Robust.Client.Player;
using Robust.Shared.Prototypes;
namespace Content.Client.Zombies;
public sealed class ZombieSystem : SharedZombieSystem
{
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ZombieComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<ZombieComponent, GetStatusIconsEvent>(OnGetStatusIcon);
}
private void OnStartup(EntityUid uid, ZombieComponent component, ComponentStartup args)
{
if (HasComp<HumanoidAppearanceComponent>(uid))
return;
if (!TryComp<SpriteComponent>(uid, out var sprite))
return;
for (var i = 0; i < sprite.AllLayers.Count(); i++)
{
sprite.LayerSetColor(i, component.SkinColor);
}
}
private void OnGetStatusIcon(EntityUid uid, ZombieComponent component, ref GetStatusIconsEvent args)
{
if (!HasComp<ZombieComponent>(_player.LocalPlayer?.ControlledEntity))
return;
args.StatusIcons.Add(_prototype.Index<StatusIconPrototype>(component.ZombieStatusIcon));
}
}

View File

@@ -5,14 +5,13 @@ using Content.Shared.Administration;
using Content.Shared.Database;
using Content.Shared.Verbs;
using Robust.Server.GameObjects;
using Robust.Shared.Configuration;
using Robust.Shared.Utility;
namespace Content.Server.Administration.Systems;
public sealed partial class AdminVerbSystem
{
[Dependency] private readonly ZombifyOnDeathSystem _zombify = default!;
[Dependency] private readonly ZombieSystem _zombie = default!;
[Dependency] private readonly TraitorRuleSystem _traitorRule = default!;
[Dependency] private readonly NukeopsRuleSystem _nukeopsRule = default!;
[Dependency] private readonly PiratesRuleSystem _piratesRule = default!;
@@ -53,10 +52,10 @@ public sealed partial class AdminVerbSystem
{
Text = "Make Zombie",
Category = VerbCategory.Antag,
Icon = new SpriteSpecifier.Rsi(new("/Textures/Structures/Wallmounts/signs.rsi"), "bio"),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/Actions/zombie-turn.png")),
Act = () =>
{
_zombify.ZombifyEntity(args.Target);
_zombie.ZombifyEntity(args.Target);
},
Impact = LogImpact.High,
Message = Loc.GetString("admin-verb-make-zombie"),

View File

@@ -9,24 +9,28 @@ namespace Content.Server.Chemistry.ReagentEffects;
public sealed class CureZombieInfection : ReagentEffect
{
[DataField("innoculate")]
public bool innoculate = false;
public bool Innoculate;
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
{
if(innoculate == true)
return Loc.GetString("reagent-effect-guidebook-innoculate-zombie-infection", ("chance", Probability));
return Loc.GetString("reagent-effect-guidebook-cure-zombie-infection", ("chance", Probability));
if(Innoculate)
return Loc.GetString("reagent-effect-guidebook-innoculate-zombie-infection", ("chance", Probability));
return Loc.GetString("reagent-effect-guidebook-cure-zombie-infection", ("chance", Probability));
}
// Removes the Zombie Infection Components
public override void Effect(ReagentEffectArgs args)
{
var entityManager = args.EntityManager;
if (entityManager.HasComponent<IncurableZombieComponent>(args.SolutionEntity))
return;
entityManager.RemoveComponent<ZombifyOnDeathComponent>(args.SolutionEntity);
entityManager.RemoveComponent<PendingZombieComponent>(args.SolutionEntity);
if (innoculate == true) {
if (Innoculate)
{
entityManager.EnsureComponent<ZombieImmuneComponent>(args.SolutionEntity);
}
}

View File

@@ -1,35 +1,97 @@
namespace Content.Server.GameTicking.Rules.Components;
using Content.Shared.Roles;
using Robust.Shared.Audio;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.GameTicking.Rules.Components;
[RegisterComponent, Access(typeof(ZombieRuleSystem))]
public sealed class ZombieRuleComponent : Component
{
[DataField("initialInfectedNames")]
public Dictionary<string, string> InitialInfectedNames = new();
public string PatientZeroPrototypeID = "InitialInfected";
[DataField("patientZeroPrototypeId", customTypeSerializer: typeof(PrototypeIdSerializer<AntagPrototype>))]
public string PatientZeroPrototypeId = "InitialInfected";
/// <summary>
/// Whether or not the initial infected have been chosen.
/// </summary>
[DataField("infectedChosen")]
public bool InfectedChosen;
/// <summary>
/// When the round will next check for round end.
/// </summary>
[DataField("nextRoundEndCheck", customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan NextRoundEndCheck;
/// <summary>
/// The amount of time between each check for the end of the round.
/// </summary>
[DataField("endCheckDelay")]
public TimeSpan EndCheckDelay = TimeSpan.FromSeconds(30);
/// <summary>
/// The time at which the initial infected will be chosen.
/// </summary>
[DataField("startTime", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
public TimeSpan? StartTime;
/// <summary>
/// The minimum amount of time after the round starts that the initial infected will be chosen.
/// </summary>
[DataField("minStartDelay")]
public TimeSpan MinStartDelay = TimeSpan.FromMinutes(10);
/// <summary>
/// The maximum amount of time after the round starts that the initial infected will be chosen.
/// </summary>
[DataField("maxStartDelay")]
public TimeSpan MaxStartDelay = TimeSpan.FromMinutes(15);
/// <summary>
/// The sound that plays when someone becomes an initial infected.
/// todo: this should have a unique sound instead of reusing the zombie one.
/// </summary>
[DataField("initialInfectedSound")]
public SoundSpecifier InitialInfectedSound = new SoundPathSpecifier("/Audio/Ambience/Antag/zombie_start.ogg");
/// <summary>
/// The minimum amount of time initial infected have before they start taking infection damage.
/// </summary>
[DataField("minInitialInfectedGrace")]
public TimeSpan MinInitialInfectedGrace = TimeSpan.FromMinutes(12.5f);
/// <summary>
/// The maximum amount of time initial infected have before they start taking damage.
/// </summary>
[DataField("maxInitialInfectedGrace")]
public TimeSpan MaxInitialInfectedGrace = TimeSpan.FromMinutes(15f);
/// <summary>
/// How many players for each initial infected.
/// </summary>
[DataField("playersPerInfected")]
public int PlayersPerInfected = 10;
/// <summary>
/// The maximum number of initial infected.
/// </summary>
[DataField("maxInitialInfected")]
public int MaxInitialInfected = 6;
/// <summary>
/// After this amount of the crew become zombies, the shuttle will be automatically called.
/// </summary>
[DataField("zombieShuttleCallPercentage")]
public float ZombieShuttleCallPercentage = 0.5f;
/// <summary>
/// Have we called the evac shuttle yet?
/// </summary>
[DataField("shuttleCalled")]
public bool ShuttleCalled;
public const string ZombifySelfActionPrototype = "TurnUndead";
/// <summary>
/// After this many seconds the players will be forced to turn into zombies (at minimum)
/// Defaults to 20 minutes. 20*60 = 1200 seconds.
///
/// Zombie time for a given player is:
/// random MinZombieForceSecs to MaxZombieForceSecs + up to PlayerZombieForceVariation
/// </summary>
[DataField("minZombieForceSecs"), ViewVariables(VVAccess.ReadWrite)]
public float MinZombieForceSecs = 1200;
/// <summary>
/// After this many seconds the players will be forced to turn into zombies (at maximum)
/// Defaults to 30 minutes. 30*60 = 1800 seconds.
/// </summary>
[DataField("maxZombieForceSecs"), ViewVariables(VVAccess.ReadWrite)]
public float MaxZombieForceSecs = 1800;
/// <summary>
/// How many additional seconds each player will get (at random) to scatter forced zombies over time.
/// Defaults to 2 minutes. 2*60 = 120 seconds.
/// </summary>
[DataField("playerZombieForceVariationSecs"), ViewVariables(VVAccess.ReadWrite)]
public float PlayerZombieForceVariationSecs = 120;
}

View File

@@ -29,6 +29,7 @@ using Content.Shared.Mobs.Components;
using Content.Shared.Nuke;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Content.Shared.Zombies;
using Robust.Server.GameObjects;
using Robust.Server.Maps;
using Robust.Server.Player;
@@ -74,6 +75,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
SubscribeLocalEvent<NukeOperativeComponent, MindAddedMessage>(OnMindAdded);
SubscribeLocalEvent<NukeOperativeComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<NukeOperativeComponent, ComponentRemove>(OnComponentRemove);
SubscribeLocalEvent<NukeOperativeComponent, EntityZombifiedEvent>(OnOperativeZombified);
}
private void OnComponentInit(EntityUid uid, NukeOperativeComponent component, ComponentInit args)
@@ -100,6 +102,11 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
CheckRoundShouldEnd();
}
private void OnOperativeZombified(EntityUid uid, NukeOperativeComponent component, ref EntityZombifiedEvent args)
{
RemCompDeferred(uid, component);
}
private void OnNukeExploded(NukeExplodedEvent ev)
{
var query = EntityQueryEnumerator<NukeopsRuleComponent, GameRuleComponent>();

View File

@@ -2,6 +2,7 @@ using System.Globalization;
using System.Linq;
using Content.Server.Actions;
using Content.Server.Chat.Managers;
using Content.Server.Chat.Systems;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Mind;
using Content.Server.Mind.Components;
@@ -10,7 +11,8 @@ using Content.Server.Popups;
using Content.Server.Preferences.Managers;
using Content.Server.Roles;
using Content.Server.RoundEnd;
using Content.Server.Traitor;
using Content.Server.Station.Components;
using Content.Server.Station.Systems;
using Content.Server.Zombies;
using Content.Shared.Actions.ActionTypes;
using Content.Shared.CCVar;
@@ -21,11 +23,12 @@ using Content.Shared.Mobs.Systems;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Content.Shared.Zombies;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Utility;
using Robust.Shared.Timing;
namespace Content.Server.GameTicking.Rules;
@@ -35,25 +38,25 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IServerPreferencesManager _prefs = default!;
[Dependency] private readonly RoundEndSystem _roundEndSystem = default!;
[Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly RoundEndSystem _roundEnd = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly ActionsSystem _action = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly ZombifyOnDeathSystem _zombify = default!;
[Dependency] private readonly ZombieSystem _zombie = default!;
[Dependency] private readonly MindSystem _mindSystem = default!;
[Dependency] private readonly StationSystem _station = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RoundStartAttemptEvent>(OnStartAttempt);
SubscribeLocalEvent<MobStateChangedEvent>(OnMobStateChanged);
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndText);
SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(OnJobAssigned);
SubscribeLocalEvent<EntityZombifiedEvent>(OnEntityZombified);
SubscribeLocalEvent<ZombifyOnDeathComponent, ZombifySelfActionEvent>(OnZombifySelf);
}
@@ -62,7 +65,7 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
foreach (var zombie in EntityQuery<ZombieRuleComponent>())
{
// This is just the general condition thing used for determining the win/lose text
var fraction = GetInfectedFraction();
var fraction = GetInfectedFraction(true, true);
if (fraction <= 0)
ev.AddLine(Loc.GetString("zombie-round-end-amount-none"));
@@ -86,80 +89,63 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
var healthy = GetHealthyHumans();
// Gets a bunch of the living players and displays them if they're under a threshold.
// InitialInfected is used for the threshold because it scales with the player count well.
if (healthy.Count > 0 && healthy.Count <= 2 * zombie.InitialInfectedNames.Count)
if (healthy.Count <= 0 || healthy.Count > 2 * zombie.InitialInfectedNames.Count)
continue;
ev.AddLine("");
ev.AddLine(Loc.GetString("zombie-round-end-survivor-count", ("count", healthy.Count)));
foreach (var survivor in healthy)
{
ev.AddLine("");
ev.AddLine(Loc.GetString("zombie-round-end-survivor-count", ("count", healthy.Count)));
foreach (var survivor in healthy)
var meta = MetaData(survivor);
var username = string.Empty;
if (TryComp<MindContainerComponent>(survivor, out var mindcomp))
{
var meta = MetaData(survivor);
var username = string.Empty;
if (TryComp<MindContainerComponent>(survivor, out var mindcomp))
if (mindcomp.Mind != null && mindcomp.Mind.Session != null)
username = mindcomp.Mind.Session.Name;
ev.AddLine(Loc.GetString("zombie-round-end-user-was-survivor",
("name", meta.EntityName),
("username", username)));
if (mindcomp.Mind != null && mindcomp.Mind.Session != null)
username = mindcomp.Mind.Session.Name;
}
ev.AddLine(Loc.GetString("zombie-round-end-user-was-survivor",
("name", meta.EntityName),
("username", username)));
}
}
}
private void OnJobAssigned(RulePlayerJobsAssignedEvent ev)
{
var query = EntityQueryEnumerator<ZombieRuleComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out var zombies, out var gameRule))
{
if (!GameTicker.IsGameRuleAdded(uid, gameRule))
continue;
InfectInitialPlayers(zombies);
}
}
/// <remarks>
/// This is just checked if the last human somehow dies
/// by starving or flying off into space.
/// </remarks>
private void OnMobStateChanged(MobStateChangedEvent ev)
{
CheckRoundEnd(ev.Target);
}
private void OnEntityZombified(EntityZombifiedEvent ev)
{
CheckRoundEnd(ev.Target);
}
/// <summary>
/// The big kahoona function for checking if the round is gonna end
/// </summary>
/// <param name="target">depending on this uid, we should care about the round ending</param>
private void CheckRoundEnd(EntityUid target)
private void CheckRoundEnd()
{
var query = EntityQueryEnumerator<ZombieRuleComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out var zombies, out var gameRule))
while (query.MoveNext(out var uid, out var comp, out var gameRule))
{
if (GameTicker.IsGameRuleActive(uid, gameRule))
if (!GameTicker.IsGameRuleActive(uid, gameRule))
continue;
// We only care about players, not monkeys and such.
if (!HasComp<HumanoidAppearanceComponent>(target))
continue;
var fraction = GetInfectedFraction();
var healthy = GetHealthyHumans();
if (healthy.Count == 1) // Only one human left. spooky
_popup.PopupEntity(Loc.GetString("zombie-alone"), healthy[0], healthy[0]);
if (fraction >= 1) // Oops, all zombies
_roundEndSystem.EndRound();
if (!comp.ShuttleCalled && GetInfectedFraction(false) >= comp.ZombieShuttleCallPercentage)
{
comp.ShuttleCalled = true;
foreach (var station in _station.GetStations())
{
_chat.DispatchStationAnnouncement(station, Loc.GetString("zombie-shuttle-call"), colorOverride: Color.Crimson);
}
_roundEnd.RequestRoundEnd(null, false);
}
// we include dead for this count because we don't want to end the round
// when everyone gets on the shuttle.
if (GetInfectedFraction() >= 1) // Oops, all zombies
_roundEnd.EndRound();
}
}
private void OnStartAttempt(RoundStartAttemptEvent ev)
{
var query = EntityQueryEnumerator<ZombieRuleComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out var zombies, out var gameRule))
while (query.MoveNext(out var uid, out _, out var gameRule))
{
if (!GameTicker.IsGameRuleAdded(uid, gameRule))
continue;
@@ -185,37 +171,86 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
protected override void Started(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
{
base.Started(uid, component, gameRule, args);
component.StartTime = _timing.CurTime + _random.Next(component.MinStartDelay, component.MaxStartDelay);
}
protected override void ActiveTick(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, float frameTime)
{
base.ActiveTick(uid, component, gameRule, frameTime);
if (component.InfectedChosen)
{
if (_timing.CurTime >= component.NextRoundEndCheck)
{
component.NextRoundEndCheck += component.EndCheckDelay;
CheckRoundEnd();
}
return;
}
if (component.StartTime == null || _timing.CurTime < component.StartTime)
return;
InfectInitialPlayers(component);
}
private void OnZombifySelf(EntityUid uid, ZombifyOnDeathComponent component, ZombifySelfActionEvent args)
{
_zombify.ZombifyEntity(uid);
_zombie.ZombifyEntity(uid);
var action = new InstantAction(_prototypeManager.Index<InstantActionPrototype>(ZombieRuleComponent.ZombifySelfActionPrototype));
_action.RemoveAction(uid, action);
}
private float GetInfectedFraction()
private float GetInfectedFraction(bool includeOffStation = true, bool includeDead = false)
{
var players = EntityQuery<HumanoidAppearanceComponent>(true);
var zombers = EntityQuery<HumanoidAppearanceComponent, ZombieComponent>(true);
var players = GetHealthyHumans(includeOffStation);
var zombieCount = 0;
var query = EntityQueryEnumerator<HumanoidAppearanceComponent, ZombieComponent, MobStateComponent>();
while (query.MoveNext(out _, out _, out _, out var mob))
{
if (!includeDead && mob.CurrentState == MobState.Dead)
continue;
zombieCount++;
}
return zombers.Count() / (float) players.Count();
return zombieCount / (float) (players.Count + zombieCount);
}
private List<EntityUid> GetHealthyHumans()
/// <summary>
/// Gets the list of humans who are alive, not zombies, and are on a station.
/// Flying off via a shuttle disqualifies you.
/// </summary>
/// <returns></returns>
private List<EntityUid> GetHealthyHumans(bool includeOffStation = true)
{
var healthy = new List<EntityUid>();
var players = AllEntityQuery<HumanoidAppearanceComponent, MobStateComponent>();
var zombers = GetEntityQuery<ZombieComponent>();
while (players.MoveNext(out var uid, out _, out var mob))
var stationGrids = new HashSet<EntityUid>();
if (!includeOffStation)
{
if (_mobState.IsAlive(uid, mob) && !zombers.HasComponent(uid))
foreach (var station in _station.GetStationsSet())
{
healthy.Add(uid);
if (TryComp<StationDataComponent>(station, out var data) && _station.GetLargestGrid(data) is { } grid)
stationGrids.Add(grid);
}
}
var players = AllEntityQuery<HumanoidAppearanceComponent, ActorComponent, MobStateComponent, TransformComponent>();
var zombers = GetEntityQuery<ZombieComponent>();
while (players.MoveNext(out var uid, out _, out _, out var mob, out var xform))
{
if (!_mobState.IsAlive(uid, mob))
continue;
if (zombers.HasComponent(uid))
continue;
if (!includeOffStation && !stationGrids.Contains(xform.GridUid ?? EntityUid.Invalid))
continue;
healthy.Add(uid);
}
return healthy;
}
@@ -230,95 +265,80 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
/// </remarks>
private void InfectInitialPlayers(ZombieRuleComponent component)
{
if (component.InfectedChosen)
return;
component.InfectedChosen = true;
var allPlayers = _playerManager.ServerSessions.ToList();
var playerList = new List<IPlayerSession>();
var prefList = new List<IPlayerSession>();
foreach (var player in allPlayers)
{
// TODO: A
if (player.AttachedEntity != null && HasComp<HumanoidAppearanceComponent>(player.AttachedEntity))
{
playerList.Add(player);
if (player.AttachedEntity == null || !HasComp<HumanoidAppearanceComponent>(player.AttachedEntity))
continue;
playerList.Add(player);
var pref = (HumanoidCharacterProfile) _prefs.GetPreferences(player.UserId).SelectedCharacter;
if (pref.AntagPreferences.Contains(component.PatientZeroPrototypeID))
prefList.Add(player);
}
var pref = (HumanoidCharacterProfile) _prefs.GetPreferences(player.UserId).SelectedCharacter;
if (pref.AntagPreferences.Contains(component.PatientZeroPrototypeId))
prefList.Add(player);
}
if (playerList.Count == 0)
return;
var playersPerInfected = _cfg.GetCVar(CCVars.ZombiePlayersPerInfected);
var maxInfected = _cfg.GetCVar(CCVars.ZombieMaxInitialInfected);
var numInfected = Math.Max(1,
(int) Math.Min(
Math.Floor((double) playerList.Count / playersPerInfected), maxInfected));
Math.Floor((double) playerList.Count / component.PlayersPerInfected), component.MaxInitialInfected));
// How long the zombies have as a group to decide to begin their attack.
// Varies randomly from 20 to 30 minutes. After this the virus begins and they start
// taking zombie virus damage.
var groupTimelimit = _random.NextFloat(component.MinZombieForceSecs, component.MaxZombieForceSecs);
for (var i = 0; i < numInfected; i++)
var totalInfected = 0;
while (totalInfected < numInfected)
{
IPlayerSession zombie;
if (prefList.Count == 0)
{
if (playerList.Count == 0)
{
Logger.InfoS("preset", "Insufficient number of players. stopping selection.");
Log.Info("Insufficient number of players. stopping selection.");
break;
}
zombie = _random.PickAndTake(playerList);
Logger.InfoS("preset", "Insufficient preferred patient 0, picking at random.");
zombie = _random.Pick(playerList);
Log.Info("Insufficient preferred patient 0, picking at random.");
}
else
{
zombie = _random.PickAndTake(prefList);
playerList.Remove(zombie);
Logger.InfoS("preset", "Selected a patient 0.");
zombie = _random.Pick(prefList);
Log.Info("Selected a patient 0.");
}
var mind = zombie.Data.ContentData()?.Mind;
if (mind == null)
{
Logger.ErrorS("preset", "Failed getting mind for picked patient 0.");
prefList.Remove(zombie);
playerList.Remove(zombie);
if (zombie.Data.ContentData()?.Mind is not { } mind || mind.OwnedEntity is not { } ownedEntity)
continue;
}
DebugTools.AssertNotNull(mind.OwnedEntity);
_mindSystem.AddRole(mind, new ZombieRole(mind, _prototypeManager.Index<AntagPrototype>(component.PatientZeroPrototypeID)));
totalInfected++;
var inCharacterName = string.Empty;
// Create some variation between the times of each zombie, relative to the time of the group as a whole.
var personalDelay = _random.NextFloat(0.0f, component.PlayerZombieForceVariationSecs);
if (mind.OwnedEntity != null)
{
var pending = EnsureComp<PendingZombieComponent>(mind.OwnedEntity.Value);
// Only take damage after this many seconds
pending.InfectedSecs = -(int)(groupTimelimit + personalDelay);
EnsureComp<ZombifyOnDeathComponent>(mind.OwnedEntity.Value);
inCharacterName = MetaData(mind.OwnedEntity.Value).EntityName;
_mindSystem.AddRole(mind, new ZombieRole(mind, _prototypeManager.Index<AntagPrototype>(component.PatientZeroPrototypeId)));
var action = new InstantAction(_prototypeManager.Index<InstantActionPrototype>(ZombieRuleComponent.ZombifySelfActionPrototype));
_action.AddAction(mind.OwnedEntity.Value, action, null);
}
var pending = EnsureComp<PendingZombieComponent>(ownedEntity);
pending.GracePeriod = _random.Next(component.MinInitialInfectedGrace, component.MaxInitialInfectedGrace);
EnsureComp<ZombifyOnDeathComponent>(ownedEntity);
EnsureComp<IncurableZombieComponent>(ownedEntity);
var inCharacterName = MetaData(ownedEntity).EntityName;
var action = new InstantAction(_prototypeManager.Index<InstantActionPrototype>(ZombieRuleComponent.ZombifySelfActionPrototype));
_action.AddAction(mind.OwnedEntity.Value, action, null);
if (mind.Session != null)
{
var message = Loc.GetString("zombie-patientzero-role-greeting");
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", message));
var message = Loc.GetString("zombie-patientzero-role-greeting");
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", message));
//gets the names now in case the players leave.
//this gets unhappy if people with the same name get chose. Probably shouldn't happen.
component.InitialInfectedNames.Add(inCharacterName, mind.Session.Name);
//gets the names now in case the players leave.
//this gets unhappy if people with the same name get chosen. Probably shouldn't happen.
component.InitialInfectedNames.Add(inCharacterName, zombie.Name);
// I went all the way to ChatManager.cs and all i got was this lousy T-shirt
// You got a free T-shirt!?!?
_chatManager.ChatMessageToOne(Shared.Chat.ChatChannel.Server, message,
wrappedMessage, default, false, mind.Session.ConnectedClient, Color.Plum);
}
// I went all the way to ChatManager.cs and all i got was this lousy T-shirt
// You got a free T-shirt!?!?
_chatManager.ChatMessageToOne(Shared.Chat.ChatChannel.Server, message,
wrappedMessage, default, false, zombie.ConnectedClient, Color.Plum);
_audio.PlayGlobal(component.InitialInfectedSound, ownedEntity);
}
}
}

View File

@@ -0,0 +1,10 @@
namespace Content.Server.Zombies;
/// <summary>
/// This is used for a zombie that cannot be cured by any methods.
/// </summary>
[RegisterComponent]
public sealed class IncurableZombieComponent : Component
{
}

View File

@@ -1,5 +1,4 @@
using Content.Shared.Damage;
using Content.Shared.FixedPoint;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Zombies;
@@ -10,49 +9,46 @@ namespace Content.Server.Zombies;
[RegisterComponent]
public sealed class PendingZombieComponent : Component
{
/// <summary>
/// Damage dealt every second to infected individuals.
/// </summary>
[DataField("damage")] public DamageSpecifier Damage = new()
{
DamageDict = new ()
{
{ "Blunt", 0.8 },
{ "Toxin", 0.2 },
{ "Blunt", 0.25 },
{ "Poison", 0.1 },
}
};
/// <summary>
/// A multiplier for <see cref="Damage"/> applied when the entity is in critical condition.
/// </summary>
[DataField("critDamageMultiplier")]
public float CritDamageMultiplier = 10f;
[DataField("nextTick", customTypeSerializer:typeof(TimeOffsetSerializer))]
public TimeSpan NextTick;
/// <summary>
/// Scales damage over time.
/// The amount of time left before the infected begins to take damage.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("infectedSecs")]
public int InfectedSecs;
[DataField("gracePeriod"), ViewVariables(VVAccess.ReadWrite)]
public TimeSpan GracePeriod = TimeSpan.Zero;
/// <summary>
/// Number of seconds that a typical infection will last before the player is totally overwhelmed with damage and
/// dies.
/// The chance each second that a warning will be shown.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("maxInfectionLength")]
public float MaxInfectionLength = 120f;
[DataField("infectionWarningChance")]
public float InfectionWarningChance = 0.0166f;
/// <summary>
/// Infection warnings are shown as popups, times are in seconds.
/// -ve times shown to initial zombies (once timer counts from -ve to 0 the infection starts)
/// +ve warnings are in seconds after being bitten
/// Infection warnings shown as popups
/// </summary>
[DataField("infectionWarnings")]
public Dictionary<int, string> InfectionWarnings = new()
public List<string> InfectionWarnings = new()
{
{-45, "zombie-infection-warning"},
{-30, "zombie-infection-warning"},
{10, "zombie-infection-underway"},
{25, "zombie-infection-underway"},
"zombie-infection-warning",
"zombie-infection-underway"
};
/// <summary>
/// A minimum multiplier applied to Damage once you are in crit to get you dead and ready for your next life
/// as fast as possible.
/// </summary>
[DataField("minimumCritMultiplier")]
public float MinimumCritMultiplier = 10;
}

View File

@@ -1,8 +1,8 @@
namespace Content.Server.Zombies
namespace Content.Server.Zombies;
[RegisterComponent]
public sealed class ZombieImmuneComponent : Component
{
[RegisterComponent]
public sealed class ZombieImmuneComponent : Component
{
//still no
}
//still no
}

View File

@@ -1,10 +1,7 @@
using Content.Server.Administration.Commands;
using Content.Server.Atmos.Components;
using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Server.Chat;
using Content.Server.Chat.Managers;
using Content.Server.Chat.Systems;
using Content.Server.Ghost.Roles.Components;
using Content.Server.Humanoid;
using Content.Server.IdentityManagement;
@@ -12,28 +9,33 @@ using Content.Server.Inventory;
using Content.Server.Mind.Commands;
using Content.Server.Mind.Components;
using Content.Server.Nutrition.Components;
using Content.Server.Popups;
using Content.Server.Roles;
using Content.Server.Speech.Components;
using Content.Server.Temperature.Components;
using Content.Server.Traitor;
using Content.Shared.CombatMode;
using Content.Shared.Damage;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Server.Mind;
using Content.Server.NPC;
using Content.Server.NPC.Components;
using Content.Server.NPC.HTN;
using Content.Server.NPC.Systems;
using Content.Server.RoundEnd;
using Content.Shared.Humanoid;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems;
using Content.Shared.Nutrition.Components;
using Content.Shared.Popups;
using Content.Shared.Roles;
using Content.Shared.Tools;
using Content.Shared.Tools.Components;
using Content.Shared.Weapons.Melee;
using Content.Shared.Zombies;
using Robust.Shared.Prototypes;
using Robust.Shared.Audio;
using Robust.Shared.Utility;
namespace Content.Server.Zombies
{
@@ -43,32 +45,20 @@ namespace Content.Server.Zombies
/// <remarks>
/// Don't Shitcode Open Inside
/// </remarks>
public sealed class ZombifyOnDeathSystem : EntitySystem
public sealed partial class ZombieSystem
{
[Dependency] private readonly SharedHandsSystem _sharedHands = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly BloodstreamSystem _bloodstream = default!;
[Dependency] private readonly ServerInventorySystem _serverInventory = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly HumanoidAppearanceSystem _sharedHuApp = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly ServerInventorySystem _inventory = default!;
[Dependency] private readonly NpcFactionSystem _faction = default!;
[Dependency] private readonly NPCSystem _npc = default!;
[Dependency] private readonly HumanoidAppearanceSystem _humanoidAppearance = default!;
[Dependency] private readonly IdentitySystem _identity = default!;
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
[Dependency] private readonly AutoEmoteSystem _autoEmote = default!;
[Dependency] private readonly EmoteOnDamageSystem _emoteOnDamage = default!;
[Dependency] private readonly SharedCombatModeSystem _combat = default!;
[Dependency] private readonly IChatManager _chatMan = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly MobThresholdSystem _mobThreshold = default!;
[Dependency] private readonly MindSystem _mindSystem = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ZombifyOnDeathComponent, MobStateChangedEvent>(OnDamageChanged);
}
[Dependency] private readonly SharedAudioSystem _audio = default!;
/// <summary>
/// Handles an entity turning into a zombie when they die or go into crit
@@ -86,6 +76,7 @@ namespace Content.Server.Zombies
/// It handles both humanoid and nonhumanoid transformation and everything should be called through it.
/// </summary>
/// <param name="target">the entity being zombified</param>
/// <param name="mobState"></param>
/// <remarks>
/// ALRIGHT BIG BOY. YOU'VE COME TO THE LAYER OF THE BEAST. THIS IS YOUR WARNING.
/// This function is the god function for zombie stuff, and it is cursed. I have
@@ -126,8 +117,7 @@ namespace Content.Server.Zombies
var melee = EnsureComp<MeleeWeaponComponent>(target);
melee.ClickAnimation = zombiecomp.AttackAnimation;
melee.WideAnimation = zombiecomp.AttackAnimation;
melee.Range = 1.5f;
Dirty(melee);
melee.Range = 1.2f;
if (mobState.CurrentState == MobState.Alive)
{
@@ -149,24 +139,38 @@ namespace Content.Server.Zombies
if (TryComp<BloodstreamComponent>(target, out var stream))
zombiecomp.BeforeZombifiedBloodReagent = stream.BloodReagent;
_sharedHuApp.SetSkinColor(target, zombiecomp.SkinColor, verify: false, humanoid: huApComp);
_sharedHuApp.SetBaseLayerColor(target, HumanoidVisualLayers.Eyes, zombiecomp.EyeColor, humanoid: huApComp);
_humanoidAppearance.SetSkinColor(target, zombiecomp.SkinColor, verify: false, humanoid: huApComp);
_humanoidAppearance.SetBaseLayerColor(target, HumanoidVisualLayers.Eyes, zombiecomp.EyeColor, humanoid: huApComp);
// this might not resync on clone?
_sharedHuApp.SetBaseLayerId(target, HumanoidVisualLayers.Tail, zombiecomp.BaseLayerExternal, humanoid: huApComp);
_sharedHuApp.SetBaseLayerId(target, HumanoidVisualLayers.HeadSide, zombiecomp.BaseLayerExternal, humanoid: huApComp);
_sharedHuApp.SetBaseLayerId(target, HumanoidVisualLayers.HeadTop, zombiecomp.BaseLayerExternal, humanoid: huApComp);
_sharedHuApp.SetBaseLayerId(target, HumanoidVisualLayers.Snout, zombiecomp.BaseLayerExternal, humanoid: huApComp);
_humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.Tail, zombiecomp.BaseLayerExternal, humanoid: huApComp);
_humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.HeadSide, zombiecomp.BaseLayerExternal, humanoid: huApComp);
_humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.HeadTop, zombiecomp.BaseLayerExternal, humanoid: huApComp);
_humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.Snout, zombiecomp.BaseLayerExternal, humanoid: huApComp);
//This is done here because non-humanoids shouldn't get baller damage
//lord forgive me for the hardcoded damage
DamageSpecifier dspec = new();
dspec.DamageDict.Add("Slash", 13);
dspec.DamageDict.Add("Piercing", 7);
dspec.DamageDict.Add("Structural", 10);
DamageSpecifier dspec = new()
{
DamageDict = new()
{
{ "Slash", 13 },
{ "Piercing", 7 },
{ "Structural", 10 }
}
};
melee.Damage = dspec;
// humanoid zombies get to pry open doors and shit
var tool = EnsureComp<ToolComponent>(target);
tool.SpeedModifier = 0.75f;
tool.Qualities = new ("Prying");
tool.UseSound = new SoundPathSpecifier("/Audio/Items/crowbar.ogg");
Dirty(tool);
}
Dirty(melee);
//The zombie gets the assigned damage weaknesses and strengths
_damageable.SetDamageModifierSetId(target, "Zombie");
@@ -177,51 +181,60 @@ namespace Content.Server.Zombies
_bloodstream.ChangeBloodReagent(target, zombiecomp.NewBloodReagent);
//This is specifically here to combat insuls, because frying zombies on grilles is funny as shit.
_serverInventory.TryUnequip(target, "gloves", true, true);
_inventory.TryUnequip(target, "gloves", true, true);
//Should prevent instances of zombies using comms for information they shouldnt be able to have.
_serverInventory.TryUnequip(target, "ears", true, true);
_inventory.TryUnequip(target, "ears", true, true);
//popup
_popupSystem.PopupEntity(Loc.GetString("zombie-transform", ("target", target)), target, PopupType.LargeCaution);
_popup.PopupEntity(Loc.GetString("zombie-transform", ("target", target)), target, PopupType.LargeCaution);
//Make it sentient if it's an animal or something
if (!HasComp<InputMoverComponent>(target)) //this component is cursed and fucks shit up
MakeSentientCommand.MakeSentient(target, EntityManager);
MakeSentientCommand.MakeSentient(target, EntityManager);
//Make the zombie not die in the cold. Good for space zombies
if (TryComp<TemperatureComponent>(target, out var tempComp))
tempComp.ColdDamage.ClampMax(0);
// Zombies can revive themselves
_mobThreshold.SetAllowRevives(target, true);
//Heals the zombie from all the damage it took while human
if (TryComp<DamageableComponent>(target, out var damageablecomp))
_damageable.SetAllDamage(target, damageablecomp, 0);
_mobState.ChangeMobState(target, MobState.Alive);
_mobThreshold.SetAllowRevives(target, false);
// Revive them now
if (TryComp<MobStateComponent>(target, out var mobstate) && mobstate.CurrentState==MobState.Dead)
_mobState.ChangeMobState(target, MobState.Alive, mobstate);
var factionComp = EnsureComp<NpcFactionMemberComponent>(target);
foreach (var id in new List<string>(factionComp.Factions))
{
_faction.RemoveFaction(target, id);
}
_faction.AddFaction(target, "Zombie");
//gives it the funny "Zombie ___" name.
var meta = MetaData(target);
zombiecomp.BeforeZombifiedEntityName = meta.EntityName;
meta.EntityName = Loc.GetString("zombie-name-prefix", ("target", meta.EntityName));
_metaData.SetEntityName(target, Loc.GetString("zombie-name-prefix", ("target", meta.EntityName)), meta);
_identity.QueueIdentityUpdate(target);
//He's gotta have a mind
var mindComp = EnsureComp<MindContainerComponent>(target);
if (_mindSystem.TryGetMind(target, out var mind, mindComp) && _mindSystem.TryGetSession(mind, out var session))
if (_mind.TryGetMind(target, out var mind, mindComp) && _mind.TryGetSession(mind, out var session))
{
//Zombie role for player manifest
_mindSystem.AddRole(mind, new ZombieRole(mind, _proto.Index<AntagPrototype>(zombiecomp.ZombieRoleId)));
_mind.AddRole(mind, new ZombieRole(mind, _protoManager.Index<AntagPrototype>(zombiecomp.ZombieRoleId)));
//Greeting message for new bebe zombers
_chatMan.DispatchServerMessage(session, Loc.GetString("zombie-infection-greeting"));
// Notificate player about new role assignment
_audioSystem.PlayGlobal(zombiecomp.GreetSoundNotification, session);
_audio.PlayGlobal(zombiecomp.GreetSoundNotification, session);
}
else
{
var htn = EnsureComp<HTNComponent>(target);
htn.RootTask = "SimpleHostileCompound";
htn.Blackboard.SetValue(NPCBlackboard.Owner, target);
_npc.WakeNPC(target, htn);
}
if (!HasComp<GhostRoleMobSpawnerComponent>(target) && !mindComp.HasMind) //this specific component gives build test trouble so pop off, ig
@@ -236,19 +249,25 @@ namespace Content.Server.Zombies
//Goes through every hand, drops the items in it, then removes the hand
//may become the source of various bugs.
foreach (var hand in _sharedHands.EnumerateHands(target))
if (TryComp<HandsComponent>(target, out var hands))
{
_sharedHands.SetActiveHand(target, hand);
_sharedHands.DoDrop(target, hand);
_sharedHands.RemoveHand(target, hand.Name);
foreach (var hand in _hands.EnumerateHands(target))
{
_hands.SetActiveHand(target, hand, hands);
_hands.DoDrop(target, hand, handsComp: hands);
_hands.RemoveHand(target, hand.Name, hands);
}
RemComp(target, hands);
}
RemComp<HandsComponent>(target);
// No longer waiting to become a zombie:
// Requires deferral because this is (probably) the event which called ZombifyEntity in the first place.
RemCompDeferred<PendingZombieComponent>(target);
//zombie gamemode stuff
RaiseLocalEvent(new EntityZombifiedEvent(target));
var ev = new EntityZombifiedEvent(target);
RaiseLocalEvent(target, ref ev, true);
//zombies get slowdown once they convert
_movementSpeedModifier.RefreshMovementSpeedModifiers(target);
}

View File

@@ -4,10 +4,8 @@ using Content.Server.Chat;
using Content.Server.Chat.Systems;
using Content.Server.Cloning;
using Content.Server.Drone.Components;
using Content.Server.Humanoid;
using Content.Server.Inventory;
using Content.Shared.Bed.Sleep;
using Content.Shared.Chemistry.Components;
using Content.Server.Emoting.Systems;
using Content.Server.Speech.EntitySystems;
using Content.Shared.Damage;
@@ -24,20 +22,18 @@ using Robust.Shared.Timing;
namespace Content.Server.Zombies
{
public sealed class ZombieSystem : SharedZombieSystem
public sealed partial class ZombieSystem : SharedZombieSystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly BloodstreamSystem _bloodstream = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly ZombifyOnDeathSystem _zombify = default!;
[Dependency] private readonly ServerInventorySystem _inv = default!;
[Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly AutoEmoteSystem _autoEmote = default!;
[Dependency] private readonly EmoteOnDamageSystem _emoteOnDamage = default!;
[Dependency] private readonly HumanoidAppearanceSystem _humanoidSystem = default!;
[Dependency] private readonly MobThresholdSystem _mobThreshold = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
@@ -55,61 +51,46 @@ namespace Content.Server.Zombies
SubscribeLocalEvent<ZombieComponent, TryingToSleepEvent>(OnSleepAttempt);
SubscribeLocalEvent<PendingZombieComponent, MapInitEvent>(OnPendingMapInit);
SubscribeLocalEvent<PendingZombieComponent, MobStateChangedEvent>(OnPendingMobState);
SubscribeLocalEvent<ZombifyOnDeathComponent, MobStateChangedEvent>(OnDamageChanged);
}
private void OnPendingMapInit(EntityUid uid, PendingZombieComponent component, MapInitEvent args)
{
component.NextTick = _timing.CurTime;
component.NextTick = _timing.CurTime + TimeSpan.FromSeconds(1f);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<PendingZombieComponent, DamageableComponent, MobStateComponent>();
var curTime = _timing.CurTime;
var zombQuery = EntityQueryEnumerator<ZombieComponent, DamageableComponent, MobStateComponent>();
// Hurt the living infected
var query = EntityQueryEnumerator<PendingZombieComponent, DamageableComponent, MobStateComponent>();
while (query.MoveNext(out var uid, out var comp, out var damage, out var mobState))
{
// Process only once per second
if (comp.NextTick + TimeSpan.FromSeconds(1) > curTime)
if (comp.NextTick > curTime)
continue;
comp.NextTick = curTime;
comp.NextTick = curTime + TimeSpan.FromSeconds(1f);
comp.InfectedSecs += 1;
// See if there should be a warning popup for the player.
if (comp.InfectionWarnings.TryGetValue(comp.InfectedSecs, out var popupStr))
{
_popup.PopupEntity(Loc.GetString(popupStr), uid, uid);
}
if (comp.InfectedSecs < 0)
{
// This zombie has a latent virus, probably set up by ZombieRuleSystem. No damage yet.
comp.GracePeriod -= TimeSpan.FromSeconds(1f);
if (comp.GracePeriod > TimeSpan.Zero)
continue;
}
// Pain of becoming a zombie grows over time
// By scaling the number of seconds we have an accessible way to scale this exponential function.
// The function was hand tuned to 120 seconds, hence the 120 constant here.
var scaledSeconds = (120.0f / comp.MaxInfectionLength) * comp.InfectedSecs;
if (_random.Prob(comp.InfectionWarningChance))
_popup.PopupEntity(Loc.GetString(_random.Pick(comp.InfectionWarnings)), uid, uid);
// 1x at 30s, 3x at 60s, 6x at 90s, 10x at 120s. Limit at 20x so we don't gib you.
var painMultiple = Math.Min(20f, 0.1f + 0.02f * scaledSeconds + 0.0005f * scaledSeconds * scaledSeconds);
if (mobState.CurrentState == MobState.Critical)
{
// Speed up their transformation when they are (or have been) in crit by ensuring their damage
// multiplier is at least 10x
painMultiple = Math.Max(comp.MinimumCritMultiplier, painMultiple);
}
_damageable.TryChangeDamage(uid, comp.Damage * painMultiple, true, false, damage);
var multiplier = _mobState.IsCritical(uid, mobState)
? comp.CritDamageMultiplier
: 1f;
_damageable.TryChangeDamage(uid, comp.Damage * multiplier, true, false, damage);
}
// Heal the zombified
var zombQuery = EntityQueryEnumerator<ZombieComponent, DamageableComponent, MobStateComponent>();
while (zombQuery.MoveNext(out var uid, out var comp, out var damage, out var mobState))
{
// Process only once per second
@@ -118,22 +99,15 @@ namespace Content.Server.Zombies
comp.NextTick = curTime;
if (comp.Permadeath)
{
// No healing
if (_mobState.IsDead(uid, mobState))
continue;
}
if (mobState.CurrentState == MobState.Alive)
{
// Gradual healing for living zombies.
_damageable.TryChangeDamage(uid, comp.Damage, true, false, damage);
}
else if (_random.Prob(comp.ZombieReviveChance))
{
// There's a small chance to reverse all the zombie's damage (damage.Damage) in one go
_damageable.TryChangeDamage(uid, -damage.Damage, true, false, damage);
}
var multiplier = _mobState.IsCritical(uid, mobState)
? comp.PassiveHealingCritMultiplier
: 1f;
// Gradual healing for living zombies.
_damageable.TryChangeDamage(uid, comp.PassiveHealing * multiplier, true, false, damage);
}
}
@@ -176,28 +150,6 @@ namespace Content.Server.Zombies
// Stop random groaning
_autoEmote.RemoveEmote(uid, "ZombieGroan");
if (args.NewMobState == MobState.Dead)
{
// Roll to see if this zombie is not coming back.
// Note that due to damage reductions it takes a lot of hits to gib a zombie without this.
if (_random.Prob(component.ZombiePermadeathChance))
{
// You're dead! No reviving for you.
_mobThreshold.SetAllowRevives(uid, false);
component.Permadeath = true;
_popup.PopupEntity(Loc.GetString("zombie-permadeath"), uid, uid);
}
}
}
}
private void OnPendingMobState(EntityUid uid, PendingZombieComponent pending, MobStateChangedEvent args)
{
if (args.NewMobState == MobState.Critical)
{
// Immediately jump to an active virus when you crit
pending.InfectedSecs = Math.Max(0, pending.InfectedSecs);
}
}
@@ -238,7 +190,7 @@ namespace Content.Server.Zombies
private void OnMeleeHit(EntityUid uid, ZombieComponent component, MeleeHitEvent args)
{
if (!EntityManager.TryGetComponent<ZombieComponent>(args.User, out var zombieComp))
if (!TryComp<ZombieComponent>(args.User, out _))
return;
if (!args.HitEntities.Any())
@@ -254,29 +206,25 @@ namespace Content.Server.Zombies
if (HasComp<ZombieComponent>(entity))
{
args.BonusDamage = -args.BaseDamage * zombieComp.OtherZombieDamageCoefficient;
args.BonusDamage = -args.BaseDamage;
}
else
{
if (!HasComp<ZombieImmuneComponent>(entity) && _random.Prob(GetZombieInfectionChance(entity, component)))
{
var pending = EnsureComp<PendingZombieComponent>(entity);
pending.MaxInfectionLength = _random.NextFloat(0.25f, 1.0f) * component.ZombieInfectionTurnTime;
EnsureComp<PendingZombieComponent>(entity);
EnsureComp<ZombifyOnDeathComponent>(entity);
}
}
if ((mobState.CurrentState == MobState.Dead || mobState.CurrentState == MobState.Critical)
&& !HasComp<ZombieComponent>(entity))
if (_mobState.IsIncapacitated(entity, mobState) && !HasComp<ZombieComponent>(entity))
{
_zombify.ZombifyEntity(entity);
ZombifyEntity(entity);
args.BonusDamage = -args.BaseDamage;
}
else if (mobState.CurrentState == MobState.Alive) //heals when zombies bite live entities
{
var healingSolution = new Solution();
healingSolution.AddReagent("Bicaridine", 1.00); //if OP, reduce/change chem
_bloodstream.TryAddToChemicals(args.User, healingSolution);
_damageable.TryChangeDamage(uid, component.HealingOnBite, true, false);
}
}
}
@@ -286,9 +234,10 @@ namespace Content.Server.Zombies
/// </summary>
/// <param name="source">the entity having the ZombieComponent</param>
/// <param name="target">the entity you want to unzombify (different from source in case of cloning, for example)</param>
/// <param name="zombiecomp"></param>
/// <remarks>
/// this currently only restore the name and skin/eye color from before zombified
/// TODO: reverse everything else done in ZombifyEntity
/// TODO: completely rethink how zombies are done to allow reversal.
/// </remarks>
public bool UnZombify(EntityUid source, EntityUid target, ZombieComponent? zombiecomp)
{
@@ -297,13 +246,13 @@ namespace Content.Server.Zombies
foreach (var (layer, info) in zombiecomp.BeforeZombifiedCustomBaseLayers)
{
_humanoidSystem.SetBaseLayerColor(target, layer, info.Color);
_humanoidSystem.SetBaseLayerId(target, layer, info.ID);
_humanoidAppearance.SetBaseLayerColor(target, layer, info.Color);
_humanoidAppearance.SetBaseLayerId(target, layer, info.ID);
}
_humanoidSystem.SetSkinColor(target, zombiecomp.BeforeZombifiedSkinColor);
_humanoidAppearance.SetSkinColor(target, zombiecomp.BeforeZombifiedSkinColor);
_bloodstream.ChangeBloodReagent(target, zombiecomp.BeforeZombifiedBloodReagent);
MetaData(target).EntityName = zombiecomp.BeforeZombifiedEntityName;
_metaData.SetEntityName(target, zombiecomp.BeforeZombifiedEntityName);
return true;
}

View File

@@ -399,12 +399,6 @@ namespace Content.Shared.CCVar
public static readonly CVarDef<int> ZombieMinPlayers =
CVarDef.Create("zombie.min_players", 20);
public static readonly CVarDef<int> ZombieMaxInitialInfected =
CVarDef.Create("zombie.max_initial_infected", 6);
public static readonly CVarDef<int> ZombiePlayersPerInfected =
CVarDef.Create("zombie.players_per_infected", 10);
/*
* Pirates
*/

View File

@@ -1,7 +1,9 @@
using Content.Shared.Chat.Prototypes;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Damage;
using Content.Shared.Roles;
using Content.Shared.Humanoid;
using Content.Shared.StatusIcon;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
@@ -9,148 +11,135 @@ using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using static Content.Shared.Humanoid.HumanoidAppearanceState;
namespace Content.Shared.Zombies
namespace Content.Shared.Zombies;
[RegisterComponent, NetworkedComponent]
public sealed class ZombieComponent : Component
{
[RegisterComponent, NetworkedComponent]
public sealed class ZombieComponent : Component
/// <summary>
/// The baseline infection chance you have if you are completely nude
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public float MaxZombieInfectionChance = 0.50f;
/// <summary>
/// The minimum infection chance possible. This is simply to prevent
/// being invincible by bundling up.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public float MinZombieInfectionChance = 0.20f;
[ViewVariables(VVAccess.ReadWrite)]
public float ZombieMovementSpeedDebuff = 0.70f;
/// <summary>
/// The skin color of the zombie
/// </summary>
[DataField("skinColor")]
public Color SkinColor = new(0.45f, 0.51f, 0.29f);
/// <summary>
/// The eye color of the zombie
/// </summary>
[DataField("eyeColor")]
public Color EyeColor = new(0.96f, 0.13f, 0.24f);
/// <summary>
/// The base layer to apply to any 'external' humanoid layers upon zombification.
/// </summary>
[DataField("baseLayerExternal")]
public string BaseLayerExternal = "MobHumanoidMarkingMatchSkin";
/// <summary>
/// The attack arc of the zombie
/// </summary>
[DataField("attackArc", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string AttackAnimation = "WeaponArcBite";
/// <summary>
/// The role prototype of the zombie antag role
/// </summary>
[DataField("zombieRoleId", customTypeSerializer: typeof(PrototypeIdSerializer<AntagPrototype>))]
public readonly string ZombieRoleId = "Zombie";
/// <summary>
/// The EntityName of the humanoid to restore in case of cloning
/// </summary>
[DataField("beforeZombifiedEntityName"), ViewVariables(VVAccess.ReadOnly)]
public string BeforeZombifiedEntityName = string.Empty;
/// <summary>
/// The CustomBaseLayers of the humanoid to restore in case of cloning
/// </summary>
[DataField("beforeZombifiedCustomBaseLayers")]
public Dictionary<HumanoidVisualLayers, CustomBaseLayerInfo> BeforeZombifiedCustomBaseLayers = new ();
/// <summary>
/// The skin color of the humanoid to restore in case of cloning
/// </summary>
[DataField("beforeZombifiedSkinColor")]
public Color BeforeZombifiedSkinColor;
[DataField("emoteId", customTypeSerializer: typeof(PrototypeIdSerializer<EmoteSoundsPrototype>))]
public string? EmoteSoundsId = "Zombie";
public EmoteSoundsPrototype? EmoteSounds;
[DataField("nextTick", customTypeSerializer:typeof(TimeOffsetSerializer))]
public TimeSpan NextTick;
[DataField("zombieStatusIcon", customTypeSerializer: typeof(PrototypeIdSerializer<StatusIconPrototype>))]
public string ZombieStatusIcon = "ZombieFaction";
/// <summary>
/// Healing each second
/// </summary>
[DataField("passiveHealing")]
public DamageSpecifier PassiveHealing = new()
{
/// <summary>
/// The coefficient of the damage reduction applied when a zombie
/// attacks another zombie. longe name
/// </summary>
[ViewVariables]
public float OtherZombieDamageCoefficient = 0.25f;
/// <summary>
/// Chance that this zombie be permanently killed (rolled once on crit->death transition)
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public float ZombiePermadeathChance = 0.80f;
/// <summary>
/// Chance that this zombie will be healed (rolled each second when in crit or dead)
/// 3% means you have a 60% chance after 30 secs and a 84% chance after 60.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public float ZombieReviveChance = 0.03f;
/// <summary>
/// Has this zombie stopped healing now that it's died for real?
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public bool Permadeath = false;
/// <summary>
/// The baseline infection chance you have if you are completely nude
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public float MaxZombieInfectionChance = 0.50f;
/// <summary>
/// The minimum infection chance possible. This is simply to prevent
/// being invincible by bundling up.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public float MinZombieInfectionChance = 0.20f;
[ViewVariables(VVAccess.ReadWrite)]
public float ZombieMovementSpeedDebuff = 0.70f;
/// <summary>
/// How long it takes our bite victims to turn in seconds (max).
/// Will roll 25% - 100% of this on bite.
/// </summary>
[DataField("zombieInfectionTurnTime"), ViewVariables(VVAccess.ReadWrite)]
public float ZombieInfectionTurnTime = 480.0f;
/// <summary>
/// The skin color of the zombie
/// </summary>
[DataField("skinColor")]
public Color SkinColor = new(0.45f, 0.51f, 0.29f);
/// <summary>
/// The eye color of the zombie
/// </summary>
[DataField("eyeColor")]
public Color EyeColor = new(0.96f, 0.13f, 0.24f);
/// <summary>
/// The base layer to apply to any 'external' humanoid layers upon zombification.
/// </summary>
[DataField("baseLayerExternal")]
public string BaseLayerExternal = "MobHumanoidMarkingMatchSkin";
/// <summary>
/// The attack arc of the zombie
/// </summary>
[DataField("attackArc", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string AttackAnimation = "WeaponArcBite";
/// <summary>
/// The role prototype of the zombie antag role
/// </summary>
[DataField("zombieRoleId", customTypeSerializer: typeof(PrototypeIdSerializer<AntagPrototype>))]
public readonly string ZombieRoleId = "Zombie";
/// <summary>
/// The EntityName of the humanoid to restore in case of cloning
/// </summary>
[DataField("beforeZombifiedEntityName"), ViewVariables(VVAccess.ReadOnly)]
public string BeforeZombifiedEntityName = String.Empty;
/// <summary>
/// The CustomBaseLayers of the humanoid to restore in case of cloning
/// </summary>
[DataField("beforeZombifiedCustomBaseLayers")]
public Dictionary<HumanoidVisualLayers, CustomBaseLayerInfo> BeforeZombifiedCustomBaseLayers = new ();
/// <summary>
/// The skin color of the humanoid to restore in case of cloning
/// </summary>
[DataField("beforeZombifiedSkinColor")]
public Color BeforeZombifiedSkinColor;
[DataField("emoteId", customTypeSerializer: typeof(PrototypeIdSerializer<EmoteSoundsPrototype>))]
public string? EmoteSoundsId = "Zombie";
public EmoteSoundsPrototype? EmoteSounds;
[DataField("nextTick", customTypeSerializer:typeof(TimeOffsetSerializer))]
public TimeSpan NextTick;
/// <summary>
/// Healing each second
/// </summary>
[DataField("damage")] public DamageSpecifier Damage = new()
DamageDict = new ()
{
DamageDict = new ()
{
{ "Blunt", -0.4 },
{ "Slash", -0.2 },
{ "Piercing", -0.2 },
{ "Heat", -0.2 },
{ "Cold", -0.2 },
{ "Shock", -0.2 },
}
};
{ "Blunt", -0.4 },
{ "Slash", -0.2 },
{ "Piercing", -0.2 }
}
};
/// <summary>
/// Path to antagonist alert sound.
/// </summary>
[DataField("greetSoundNotification")]
public SoundSpecifier GreetSoundNotification = new SoundPathSpecifier("/Audio/Ambience/Antag/zombie_start.ogg");
/// <summary>
/// A multiplier applied to <see cref="PassiveHealing"/> when the entity is in critical condition.
/// </summary>
[DataField("passiveHealingCritMultiplier")]
public float PassiveHealingCritMultiplier = 2f;
/// <summary>
/// The blood reagent of the humanoid to restore in case of cloning
/// </summary>
[DataField("beforeZombifiedBloodReagent")]
public string BeforeZombifiedBloodReagent = String.Empty;
/// <summary>
/// Healing given when a zombie bites a living being.
/// </summary>
[DataField("healingOnBite")]
public DamageSpecifier HealingOnBite = new()
{
DamageDict = new()
{
{ "Blunt", -2 },
{ "Slash", -2 },
{ "Piercing", -2 }
}
};
/// <summary>
/// The blood reagent to give the zombie. In case you want zombies that bleed milk, or something.
/// </summary>
[DataField("newBloodReagent")]
public string NewBloodReagent = "ZombieBlood";
}
/// <summary>
/// Path to antagonist alert sound.
/// </summary>
[DataField("greetSoundNotification")]
public SoundSpecifier GreetSoundNotification = new SoundPathSpecifier("/Audio/Ambience/Antag/zombie_start.ogg");
/// <summary>
/// The blood reagent of the humanoid to restore in case of cloning
/// </summary>
[DataField("beforeZombifiedBloodReagent")]
public string BeforeZombifiedBloodReagent = string.Empty;
/// <summary>
/// The blood reagent to give the zombie. In case you want zombies that bleed milk, or something.
/// </summary>
[DataField("newBloodReagent", customTypeSerializer: typeof(PrototypeIdSerializer<ReagentPrototype>))]
public string NewBloodReagent = "ZombieBlood";
}

View File

@@ -6,6 +6,7 @@ namespace Content.Shared.Zombies;
/// Event that is broadcast whenever an entity is zombified.
/// Used by the zombie gamemode to track total infections.
/// </summary>
[ByRefEvent]
public readonly struct EntityZombifiedEvent
{
/// <summary>

View File

@@ -1,16 +1,18 @@
zombie-title = Zombies
zombie-description = A virus able to animate the dead has been unleashed unto the station! Work with your crewmates to contain the outbreak and survive.
zombie-description = The undead have been unleashed on the station! Work with the crew to survive the outbreak and secure the station.
zombie-not-enough-ready-players = Not enough players readied up for the game! There were {$readyPlayersCount} players readied up out of {$minimumPlayers} needed. Can't start Zombies.
zombie-no-one-ready = No players readied up! Can't start Zombies.
zombie-patientzero-role-greeting = You are patient 0. Hide your infection, get supplies, and be prepared to turn once you die.
zombie-patientzero-role-greeting = You are an initial infected. Get supplies and prepare for your eventual transformation. Your goal is to overtake the station while infecting as many people as possible.
zombie-healing = You feel a stirring in your flesh
zombie-infection-warning = You feel the zombie virus take hold
zombie-infection-underway = Your blood begins to thicken
zombie-alone = You feel entirely alone.
zombie-shuttle-call = We have detected that the undead have overtaken the station. Dispatching an emergency shuttle to collect remaining personnel.
zombie-round-end-initial-count = {$initialCount ->
[one] There was one initial infected:
*[other] There were {$initialCount} initial infected:

View File

@@ -1,9 +1,9 @@
zombie-transform = {CAPITALIZE(THE($target))} turned into a zombie!
zombie-infection-greeting = You have become a zombie. Your goal is to seek out the living and to try to infect them. Work together with your fellow zombies to overpower the remaining crewmates.
zombie-infection-greeting = You have become a zombie. Your goal is to seek out the living and to try to infect them. Work together with the other zombies to overtake the station.
zombie-generic = zombie
zombie-name-prefix = Zombified {$target}
zombie-role-desc = A malevolent creature of the dead.
zombie-role-rules = You are an antagonist. Search out the living and bite them in order to infect them and turn them into zombies. Work together with other the zombies to overtake the station.
zombie-role-rules = You are an antagonist. Search out the living and bite them in order to infect them and turn them into zombies. Work together with the other zombies to overtake the station.
zombie-permadeath = This time, you're dead for real.

View File

@@ -140,14 +140,14 @@
Poison: 0.8
- type: damageModifierSet
id: Zombie #Blunt resistent and immune to biological threats, but can be hacked apart and burned
id: Zombie #Blunt resistant and immune to biological threats, but can be hacked apart and burned
coefficients:
Blunt: 0.7
Slash: 1.1
Piercing: 0.9
Shock: 1.5
Shock: 1.25
Cold: 0.3
Heat: 2.0
Heat: 1.5
Poison: 0.0
Radiation: 0.0

View File

@@ -309,6 +309,11 @@
weight: 2.5
duration: 1
- type: ZombieRule
minStartDelay: 0 #let them know immediately
maxStartDelay: 10
maxInitialInfected: 3 #fewer zombies
minInitialInfectedGrace: 300 #less time to prepare
maxInitialInfectedGrace: 450
- type: entity
id: LoneOpsSpawn

View File

@@ -5,6 +5,7 @@
- Syndicate
- Xeno
- PetsNT
- Zombie
- type: npcFaction
id: NanoTrasen
@@ -12,6 +13,7 @@
- SimpleHostile
- Syndicate
- Xeno
- Zombie
- type: npcFaction
id: Mouse
@@ -26,6 +28,7 @@
hostile:
- Mouse
- SimpleHostile
- Zombie
- Xeno
- type: npcFaction
@@ -35,6 +38,7 @@
- Syndicate
- Passive
- PetsNT
- Zombie
- type: npcFaction
id: SimpleNeutral
@@ -46,6 +50,7 @@
- SimpleHostile
- Xeno
- PetsNT
- Zombie
- type: npcFaction
id: Xeno
@@ -54,3 +59,14 @@
- Syndicate
- Passive
- PetsNT
- Zombie
- type: npcFaction
id: Zombie
hostile:
- NanoTrasen
- SimpleNeutral
- SimpleHostile
- Syndicate
- Passive
- PetsNT

View File

@@ -2,5 +2,5 @@
id: Secret
weights:
Nukeops: 0.25
Traitor: 0.70
# Zombie: 0.05
Traitor: 0.65
Zombie: 0.10