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; namespace Content.Client.Zombies;
public sealed class ZombieSystem : SharedZombieSystem 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.Database;
using Content.Shared.Verbs; using Content.Shared.Verbs;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Shared.Configuration;
using Robust.Shared.Utility; using Robust.Shared.Utility;
namespace Content.Server.Administration.Systems; namespace Content.Server.Administration.Systems;
public sealed partial class AdminVerbSystem 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 TraitorRuleSystem _traitorRule = default!;
[Dependency] private readonly NukeopsRuleSystem _nukeopsRule = default!; [Dependency] private readonly NukeopsRuleSystem _nukeopsRule = default!;
[Dependency] private readonly PiratesRuleSystem _piratesRule = default!; [Dependency] private readonly PiratesRuleSystem _piratesRule = default!;
@@ -53,10 +52,10 @@ public sealed partial class AdminVerbSystem
{ {
Text = "Make Zombie", Text = "Make Zombie",
Category = VerbCategory.Antag, 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 = () => Act = () =>
{ {
_zombify.ZombifyEntity(args.Target); _zombie.ZombifyEntity(args.Target);
}, },
Impact = LogImpact.High, Impact = LogImpact.High,
Message = Loc.GetString("admin-verb-make-zombie"), Message = Loc.GetString("admin-verb-make-zombie"),

View File

@@ -9,24 +9,28 @@ namespace Content.Server.Chemistry.ReagentEffects;
public sealed class CureZombieInfection : ReagentEffect public sealed class CureZombieInfection : ReagentEffect
{ {
[DataField("innoculate")] [DataField("innoculate")]
public bool innoculate = false; public bool Innoculate;
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
{ {
if(innoculate == true) if(Innoculate)
return Loc.GetString("reagent-effect-guidebook-innoculate-zombie-infection", ("chance", Probability)); return Loc.GetString("reagent-effect-guidebook-innoculate-zombie-infection", ("chance", Probability));
return Loc.GetString("reagent-effect-guidebook-cure-zombie-infection", ("chance", Probability)); return Loc.GetString("reagent-effect-guidebook-cure-zombie-infection", ("chance", Probability));
} }
// Removes the Zombie Infection Components // Removes the Zombie Infection Components
public override void Effect(ReagentEffectArgs args) public override void Effect(ReagentEffectArgs args)
{ {
var entityManager = args.EntityManager; var entityManager = args.EntityManager;
if (entityManager.HasComponent<IncurableZombieComponent>(args.SolutionEntity))
return;
entityManager.RemoveComponent<ZombifyOnDeathComponent>(args.SolutionEntity); entityManager.RemoveComponent<ZombifyOnDeathComponent>(args.SolutionEntity);
entityManager.RemoveComponent<PendingZombieComponent>(args.SolutionEntity); entityManager.RemoveComponent<PendingZombieComponent>(args.SolutionEntity);
if (innoculate == true) { if (Innoculate)
{
entityManager.EnsureComponent<ZombieImmuneComponent>(args.SolutionEntity); 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))] [RegisterComponent, Access(typeof(ZombieRuleSystem))]
public sealed class ZombieRuleComponent : Component public sealed class ZombieRuleComponent : Component
{ {
[DataField("initialInfectedNames")]
public Dictionary<string, string> InitialInfectedNames = new(); 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"; 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.Nuke;
using Content.Shared.Preferences; using Content.Shared.Preferences;
using Content.Shared.Roles; using Content.Shared.Roles;
using Content.Shared.Zombies;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Server.Maps; using Robust.Server.Maps;
using Robust.Server.Player; using Robust.Server.Player;
@@ -74,6 +75,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
SubscribeLocalEvent<NukeOperativeComponent, MindAddedMessage>(OnMindAdded); SubscribeLocalEvent<NukeOperativeComponent, MindAddedMessage>(OnMindAdded);
SubscribeLocalEvent<NukeOperativeComponent, ComponentInit>(OnComponentInit); SubscribeLocalEvent<NukeOperativeComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<NukeOperativeComponent, ComponentRemove>(OnComponentRemove); SubscribeLocalEvent<NukeOperativeComponent, ComponentRemove>(OnComponentRemove);
SubscribeLocalEvent<NukeOperativeComponent, EntityZombifiedEvent>(OnOperativeZombified);
} }
private void OnComponentInit(EntityUid uid, NukeOperativeComponent component, ComponentInit args) private void OnComponentInit(EntityUid uid, NukeOperativeComponent component, ComponentInit args)
@@ -100,6 +102,11 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
CheckRoundShouldEnd(); CheckRoundShouldEnd();
} }
private void OnOperativeZombified(EntityUid uid, NukeOperativeComponent component, ref EntityZombifiedEvent args)
{
RemCompDeferred(uid, component);
}
private void OnNukeExploded(NukeExplodedEvent ev) private void OnNukeExploded(NukeExplodedEvent ev)
{ {
var query = EntityQueryEnumerator<NukeopsRuleComponent, GameRuleComponent>(); var query = EntityQueryEnumerator<NukeopsRuleComponent, GameRuleComponent>();

View File

@@ -2,6 +2,7 @@ using System.Globalization;
using System.Linq; using System.Linq;
using Content.Server.Actions; using Content.Server.Actions;
using Content.Server.Chat.Managers; using Content.Server.Chat.Managers;
using Content.Server.Chat.Systems;
using Content.Server.GameTicking.Rules.Components; using Content.Server.GameTicking.Rules.Components;
using Content.Server.Mind; using Content.Server.Mind;
using Content.Server.Mind.Components; using Content.Server.Mind.Components;
@@ -10,7 +11,8 @@ using Content.Server.Popups;
using Content.Server.Preferences.Managers; using Content.Server.Preferences.Managers;
using Content.Server.Roles; using Content.Server.Roles;
using Content.Server.RoundEnd; using Content.Server.RoundEnd;
using Content.Server.Traitor; using Content.Server.Station.Components;
using Content.Server.Station.Systems;
using Content.Server.Zombies; using Content.Server.Zombies;
using Content.Shared.Actions.ActionTypes; using Content.Shared.Actions.ActionTypes;
using Content.Shared.CCVar; using Content.Shared.CCVar;
@@ -21,11 +23,12 @@ using Content.Shared.Mobs.Systems;
using Content.Shared.Preferences; using Content.Shared.Preferences;
using Content.Shared.Roles; using Content.Shared.Roles;
using Content.Shared.Zombies; using Content.Shared.Zombies;
using Robust.Server.GameObjects;
using Robust.Server.Player; using Robust.Server.Player;
using Robust.Shared.Configuration; using Robust.Shared.Configuration;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Utility; using Robust.Shared.Timing;
namespace Content.Server.GameTicking.Rules; namespace Content.Server.GameTicking.Rules;
@@ -35,25 +38,25 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
[Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IChatManager _chatManager = default!; [Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IServerPreferencesManager _prefs = 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 PopupSystem _popup = default!;
[Dependency] private readonly ActionsSystem _action = default!; [Dependency] private readonly ActionsSystem _action = default!;
[Dependency] private readonly MobStateSystem _mobState = 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 MindSystem _mindSystem = default!;
[Dependency] private readonly StationSystem _station = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
SubscribeLocalEvent<RoundStartAttemptEvent>(OnStartAttempt); SubscribeLocalEvent<RoundStartAttemptEvent>(OnStartAttempt);
SubscribeLocalEvent<MobStateChangedEvent>(OnMobStateChanged);
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndText); SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndText);
SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(OnJobAssigned);
SubscribeLocalEvent<EntityZombifiedEvent>(OnEntityZombified);
SubscribeLocalEvent<ZombifyOnDeathComponent, ZombifySelfActionEvent>(OnZombifySelf); SubscribeLocalEvent<ZombifyOnDeathComponent, ZombifySelfActionEvent>(OnZombifySelf);
} }
@@ -62,7 +65,7 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
foreach (var zombie in EntityQuery<ZombieRuleComponent>()) foreach (var zombie in EntityQuery<ZombieRuleComponent>())
{ {
// This is just the general condition thing used for determining the win/lose text // 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) if (fraction <= 0)
ev.AddLine(Loc.GetString("zombie-round-end-amount-none")); ev.AddLine(Loc.GetString("zombie-round-end-amount-none"));
@@ -86,80 +89,63 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
var healthy = GetHealthyHumans(); var healthy = GetHealthyHumans();
// Gets a bunch of the living players and displays them if they're under a threshold. // 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. // 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(""); var meta = MetaData(survivor);
ev.AddLine(Loc.GetString("zombie-round-end-survivor-count", ("count", healthy.Count))); var username = string.Empty;
foreach (var survivor in healthy) if (TryComp<MindContainerComponent>(survivor, out var mindcomp))
{ {
var meta = MetaData(survivor); if (mindcomp.Mind != null && mindcomp.Mind.Session != null)
var username = string.Empty; username = mindcomp.Mind.Session.Name;
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)));
} }
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> /// <summary>
/// The big kahoona function for checking if the round is gonna end /// The big kahoona function for checking if the round is gonna end
/// </summary> /// </summary>
/// <param name="target">depending on this uid, we should care about the round ending</param> private void CheckRoundEnd()
private void CheckRoundEnd(EntityUid target)
{ {
var query = EntityQueryEnumerator<ZombieRuleComponent, GameRuleComponent>(); 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; continue;
// We only care about players, not monkeys and such.
if (!HasComp<HumanoidAppearanceComponent>(target))
continue;
var fraction = GetInfectedFraction();
var healthy = GetHealthyHumans(); var healthy = GetHealthyHumans();
if (healthy.Count == 1) // Only one human left. spooky if (healthy.Count == 1) // Only one human left. spooky
_popup.PopupEntity(Loc.GetString("zombie-alone"), healthy[0], healthy[0]); _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) private void OnStartAttempt(RoundStartAttemptEvent ev)
{ {
var query = EntityQueryEnumerator<ZombieRuleComponent, GameRuleComponent>(); 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)) if (!GameTicker.IsGameRuleAdded(uid, gameRule))
continue; continue;
@@ -185,37 +171,86 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
protected override void Started(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) protected override void Started(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
{ {
base.Started(uid, component, gameRule, 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); InfectInitialPlayers(component);
} }
private void OnZombifySelf(EntityUid uid, ZombifyOnDeathComponent component, ZombifySelfActionEvent args) private void OnZombifySelf(EntityUid uid, ZombifyOnDeathComponent component, ZombifySelfActionEvent args)
{ {
_zombify.ZombifyEntity(uid); _zombie.ZombifyEntity(uid);
var action = new InstantAction(_prototypeManager.Index<InstantActionPrototype>(ZombieRuleComponent.ZombifySelfActionPrototype)); var action = new InstantAction(_prototypeManager.Index<InstantActionPrototype>(ZombieRuleComponent.ZombifySelfActionPrototype));
_action.RemoveAction(uid, action); _action.RemoveAction(uid, action);
} }
private float GetInfectedFraction() private float GetInfectedFraction(bool includeOffStation = true, bool includeDead = false)
{ {
var players = EntityQuery<HumanoidAppearanceComponent>(true); var players = GetHealthyHumans(includeOffStation);
var zombers = EntityQuery<HumanoidAppearanceComponent, ZombieComponent>(true); 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 healthy = new List<EntityUid>();
var players = AllEntityQuery<HumanoidAppearanceComponent, MobStateComponent>();
var zombers = GetEntityQuery<ZombieComponent>(); var stationGrids = new HashSet<EntityUid>();
while (players.MoveNext(out var uid, out _, out var mob)) 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; return healthy;
} }
@@ -230,95 +265,80 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
/// </remarks> /// </remarks>
private void InfectInitialPlayers(ZombieRuleComponent component) private void InfectInitialPlayers(ZombieRuleComponent component)
{ {
if (component.InfectedChosen)
return;
component.InfectedChosen = true;
var allPlayers = _playerManager.ServerSessions.ToList(); var allPlayers = _playerManager.ServerSessions.ToList();
var playerList = new List<IPlayerSession>(); var playerList = new List<IPlayerSession>();
var prefList = new List<IPlayerSession>(); var prefList = new List<IPlayerSession>();
foreach (var player in allPlayers) foreach (var player in allPlayers)
{ {
// TODO: A if (player.AttachedEntity == null || !HasComp<HumanoidAppearanceComponent>(player.AttachedEntity))
if (player.AttachedEntity != null && HasComp<HumanoidAppearanceComponent>(player.AttachedEntity)) continue;
{ playerList.Add(player);
playerList.Add(player);
var pref = (HumanoidCharacterProfile) _prefs.GetPreferences(player.UserId).SelectedCharacter; var pref = (HumanoidCharacterProfile) _prefs.GetPreferences(player.UserId).SelectedCharacter;
if (pref.AntagPreferences.Contains(component.PatientZeroPrototypeID)) if (pref.AntagPreferences.Contains(component.PatientZeroPrototypeId))
prefList.Add(player); prefList.Add(player);
}
} }
if (playerList.Count == 0) if (playerList.Count == 0)
return; return;
var playersPerInfected = _cfg.GetCVar(CCVars.ZombiePlayersPerInfected);
var maxInfected = _cfg.GetCVar(CCVars.ZombieMaxInitialInfected);
var numInfected = Math.Max(1, var numInfected = Math.Max(1,
(int) Math.Min( (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. var totalInfected = 0;
// Varies randomly from 20 to 30 minutes. After this the virus begins and they start while (totalInfected < numInfected)
// taking zombie virus damage.
var groupTimelimit = _random.NextFloat(component.MinZombieForceSecs, component.MaxZombieForceSecs);
for (var i = 0; i < numInfected; i++)
{ {
IPlayerSession zombie; IPlayerSession zombie;
if (prefList.Count == 0) if (prefList.Count == 0)
{ {
if (playerList.Count == 0) if (playerList.Count == 0)
{ {
Logger.InfoS("preset", "Insufficient number of players. stopping selection."); Log.Info("Insufficient number of players. stopping selection.");
break; break;
} }
zombie = _random.PickAndTake(playerList); zombie = _random.Pick(playerList);
Logger.InfoS("preset", "Insufficient preferred patient 0, picking at random."); Log.Info("Insufficient preferred patient 0, picking at random.");
} }
else else
{ {
zombie = _random.PickAndTake(prefList); zombie = _random.Pick(prefList);
playerList.Remove(zombie); Log.Info("Selected a patient 0.");
Logger.InfoS("preset", "Selected a patient 0.");
} }
var mind = zombie.Data.ContentData()?.Mind; prefList.Remove(zombie);
if (mind == null) playerList.Remove(zombie);
{ if (zombie.Data.ContentData()?.Mind is not { } mind || mind.OwnedEntity is not { } ownedEntity)
Logger.ErrorS("preset", "Failed getting mind for picked patient 0.");
continue; continue;
}
DebugTools.AssertNotNull(mind.OwnedEntity); totalInfected++;
_mindSystem.AddRole(mind, new ZombieRole(mind, _prototypeManager.Index<AntagPrototype>(component.PatientZeroPrototypeID)));
var inCharacterName = string.Empty; _mindSystem.AddRole(mind, new ZombieRole(mind, _prototypeManager.Index<AntagPrototype>(component.PatientZeroPrototypeId)));
// 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;
var action = new InstantAction(_prototypeManager.Index<InstantActionPrototype>(ZombieRuleComponent.ZombifySelfActionPrototype)); var pending = EnsureComp<PendingZombieComponent>(ownedEntity);
_action.AddAction(mind.OwnedEntity.Value, action, null); 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. //gets the names now in case the players leave.
//this gets unhappy if people with the same name get chose. Probably shouldn't happen. //this gets unhappy if people with the same name get chosen. Probably shouldn't happen.
component.InitialInfectedNames.Add(inCharacterName, mind.Session.Name); component.InitialInfectedNames.Add(inCharacterName, zombie.Name);
// I went all the way to ChatManager.cs and all i got was this lousy T-shirt // I went all the way to ChatManager.cs and all i got was this lousy T-shirt
// You got a free T-shirt!?!? // You got a free T-shirt!?!?
_chatManager.ChatMessageToOne(Shared.Chat.ChatChannel.Server, message, _chatManager.ChatMessageToOne(Shared.Chat.ChatChannel.Server, message,
wrappedMessage, default, false, mind.Session.ConnectedClient, Color.Plum); 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.Damage;
using Content.Shared.FixedPoint;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Zombies; namespace Content.Server.Zombies;
@@ -10,49 +9,46 @@ namespace Content.Server.Zombies;
[RegisterComponent] [RegisterComponent]
public sealed class PendingZombieComponent : Component public sealed class PendingZombieComponent : Component
{ {
/// <summary>
/// Damage dealt every second to infected individuals.
/// </summary>
[DataField("damage")] public DamageSpecifier Damage = new() [DataField("damage")] public DamageSpecifier Damage = new()
{ {
DamageDict = new () DamageDict = new ()
{ {
{ "Blunt", 0.8 }, { "Blunt", 0.25 },
{ "Toxin", 0.2 }, { "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))] [DataField("nextTick", customTypeSerializer:typeof(TimeOffsetSerializer))]
public TimeSpan NextTick; public TimeSpan NextTick;
/// <summary> /// <summary>
/// Scales damage over time. /// The amount of time left before the infected begins to take damage.
/// </summary> /// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("infectedSecs")] [DataField("gracePeriod"), ViewVariables(VVAccess.ReadWrite)]
public int InfectedSecs; public TimeSpan GracePeriod = TimeSpan.Zero;
/// <summary> /// <summary>
/// Number of seconds that a typical infection will last before the player is totally overwhelmed with damage and /// The chance each second that a warning will be shown.
/// dies.
/// </summary> /// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("maxInfectionLength")] [DataField("infectionWarningChance")]
public float MaxInfectionLength = 120f; public float InfectionWarningChance = 0.0166f;
/// <summary> /// <summary>
/// Infection warnings are shown as popups, times are in seconds. /// Infection warnings shown as popups
/// -ve times shown to initial zombies (once timer counts from -ve to 0 the infection starts)
/// +ve warnings are in seconds after being bitten
/// </summary> /// </summary>
[DataField("infectionWarnings")] [DataField("infectionWarnings")]
public Dictionary<int, string> InfectionWarnings = new() public List<string> InfectionWarnings = new()
{ {
{-45, "zombie-infection-warning"}, "zombie-infection-warning",
{-30, "zombie-infection-warning"}, "zombie-infection-underway"
{10, "zombie-infection-underway"},
{25, "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] //still no
public sealed class ZombieImmuneComponent : Component
{
//still no
}
} }

View File

@@ -1,10 +1,7 @@
using Content.Server.Administration.Commands;
using Content.Server.Atmos.Components; using Content.Server.Atmos.Components;
using Content.Server.Body.Components; using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Server.Chat; using Content.Server.Chat;
using Content.Server.Chat.Managers; using Content.Server.Chat.Managers;
using Content.Server.Chat.Systems;
using Content.Server.Ghost.Roles.Components; using Content.Server.Ghost.Roles.Components;
using Content.Server.Humanoid; using Content.Server.Humanoid;
using Content.Server.IdentityManagement; using Content.Server.IdentityManagement;
@@ -12,28 +9,33 @@ using Content.Server.Inventory;
using Content.Server.Mind.Commands; using Content.Server.Mind.Commands;
using Content.Server.Mind.Components; using Content.Server.Mind.Components;
using Content.Server.Nutrition.Components; using Content.Server.Nutrition.Components;
using Content.Server.Popups;
using Content.Server.Roles; using Content.Server.Roles;
using Content.Server.Speech.Components; using Content.Server.Speech.Components;
using Content.Server.Temperature.Components; using Content.Server.Temperature.Components;
using Content.Server.Traitor;
using Content.Shared.CombatMode; using Content.Shared.CombatMode;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Hands.Components; using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems; using Content.Shared.Hands.EntitySystems;
using Content.Server.Mind; 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.Humanoid;
using Content.Shared.Mobs; using Content.Shared.Mobs;
using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems; using Content.Shared.Mobs.Systems;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems; using Content.Shared.Movement.Systems;
using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.Components;
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.Roles; using Content.Shared.Roles;
using Content.Shared.Tools;
using Content.Shared.Tools.Components;
using Content.Shared.Weapons.Melee; using Content.Shared.Weapons.Melee;
using Content.Shared.Zombies; using Content.Shared.Zombies;
using Robust.Shared.Prototypes; using Robust.Shared.Audio;
using Robust.Shared.Utility;
namespace Content.Server.Zombies namespace Content.Server.Zombies
{ {
@@ -43,32 +45,20 @@ namespace Content.Server.Zombies
/// <remarks> /// <remarks>
/// Don't Shitcode Open Inside /// Don't Shitcode Open Inside
/// </remarks> /// </remarks>
public sealed class ZombifyOnDeathSystem : EntitySystem public sealed partial class ZombieSystem
{ {
[Dependency] private readonly SharedHandsSystem _sharedHands = default!; [Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!; [Dependency] private readonly ServerInventorySystem _inventory = default!;
[Dependency] private readonly BloodstreamSystem _bloodstream = default!; [Dependency] private readonly NpcFactionSystem _faction = default!;
[Dependency] private readonly ServerInventorySystem _serverInventory = default!; [Dependency] private readonly NPCSystem _npc = default!;
[Dependency] private readonly DamageableSystem _damageable = default!; [Dependency] private readonly HumanoidAppearanceSystem _humanoidAppearance = default!;
[Dependency] private readonly HumanoidAppearanceSystem _sharedHuApp = default!;
[Dependency] private readonly IdentitySystem _identity = default!; [Dependency] private readonly IdentitySystem _identity = default!;
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = 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 SharedCombatModeSystem _combat = default!;
[Dependency] private readonly IChatManager _chatMan = default!; [Dependency] private readonly IChatManager _chatMan = default!;
[Dependency] private readonly IPrototypeManager _proto = default!; [Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly MobThresholdSystem _mobThreshold = default!; [Dependency] private readonly MobThresholdSystem _mobThreshold = default!;
[Dependency] private readonly MindSystem _mindSystem = default!; [Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ZombifyOnDeathComponent, MobStateChangedEvent>(OnDamageChanged);
}
/// <summary> /// <summary>
/// Handles an entity turning into a zombie when they die or go into crit /// 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. /// It handles both humanoid and nonhumanoid transformation and everything should be called through it.
/// </summary> /// </summary>
/// <param name="target">the entity being zombified</param> /// <param name="target">the entity being zombified</param>
/// <param name="mobState"></param>
/// <remarks> /// <remarks>
/// ALRIGHT BIG BOY. YOU'VE COME TO THE LAYER OF THE BEAST. THIS IS YOUR WARNING. /// 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 /// 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); var melee = EnsureComp<MeleeWeaponComponent>(target);
melee.ClickAnimation = zombiecomp.AttackAnimation; melee.ClickAnimation = zombiecomp.AttackAnimation;
melee.WideAnimation = zombiecomp.AttackAnimation; melee.WideAnimation = zombiecomp.AttackAnimation;
melee.Range = 1.5f; melee.Range = 1.2f;
Dirty(melee);
if (mobState.CurrentState == MobState.Alive) if (mobState.CurrentState == MobState.Alive)
{ {
@@ -149,24 +139,38 @@ namespace Content.Server.Zombies
if (TryComp<BloodstreamComponent>(target, out var stream)) if (TryComp<BloodstreamComponent>(target, out var stream))
zombiecomp.BeforeZombifiedBloodReagent = stream.BloodReagent; zombiecomp.BeforeZombifiedBloodReagent = stream.BloodReagent;
_sharedHuApp.SetSkinColor(target, zombiecomp.SkinColor, verify: false, humanoid: huApComp); _humanoidAppearance.SetSkinColor(target, zombiecomp.SkinColor, verify: false, humanoid: huApComp);
_sharedHuApp.SetBaseLayerColor(target, HumanoidVisualLayers.Eyes, zombiecomp.EyeColor, humanoid: huApComp); _humanoidAppearance.SetBaseLayerColor(target, HumanoidVisualLayers.Eyes, zombiecomp.EyeColor, humanoid: huApComp);
// this might not resync on clone? // this might not resync on clone?
_sharedHuApp.SetBaseLayerId(target, HumanoidVisualLayers.Tail, zombiecomp.BaseLayerExternal, humanoid: huApComp); _humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.Tail, zombiecomp.BaseLayerExternal, humanoid: huApComp);
_sharedHuApp.SetBaseLayerId(target, HumanoidVisualLayers.HeadSide, zombiecomp.BaseLayerExternal, humanoid: huApComp); _humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.HeadSide, zombiecomp.BaseLayerExternal, humanoid: huApComp);
_sharedHuApp.SetBaseLayerId(target, HumanoidVisualLayers.HeadTop, zombiecomp.BaseLayerExternal, humanoid: huApComp); _humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.HeadTop, zombiecomp.BaseLayerExternal, humanoid: huApComp);
_sharedHuApp.SetBaseLayerId(target, HumanoidVisualLayers.Snout, 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 //This is done here because non-humanoids shouldn't get baller damage
//lord forgive me for the hardcoded damage //lord forgive me for the hardcoded damage
DamageSpecifier dspec = new(); DamageSpecifier dspec = new()
dspec.DamageDict.Add("Slash", 13); {
dspec.DamageDict.Add("Piercing", 7); DamageDict = new()
dspec.DamageDict.Add("Structural", 10); {
{ "Slash", 13 },
{ "Piercing", 7 },
{ "Structural", 10 }
}
};
melee.Damage = dspec; 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 //The zombie gets the assigned damage weaknesses and strengths
_damageable.SetDamageModifierSetId(target, "Zombie"); _damageable.SetDamageModifierSetId(target, "Zombie");
@@ -177,51 +181,60 @@ namespace Content.Server.Zombies
_bloodstream.ChangeBloodReagent(target, zombiecomp.NewBloodReagent); _bloodstream.ChangeBloodReagent(target, zombiecomp.NewBloodReagent);
//This is specifically here to combat insuls, because frying zombies on grilles is funny as shit. //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. //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 //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 //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 //Make the zombie not die in the cold. Good for space zombies
if (TryComp<TemperatureComponent>(target, out var tempComp)) if (TryComp<TemperatureComponent>(target, out var tempComp))
tempComp.ColdDamage.ClampMax(0); tempComp.ColdDamage.ClampMax(0);
// Zombies can revive themselves
_mobThreshold.SetAllowRevives(target, true); _mobThreshold.SetAllowRevives(target, true);
//Heals the zombie from all the damage it took while human //Heals the zombie from all the damage it took while human
if (TryComp<DamageableComponent>(target, out var damageablecomp)) if (TryComp<DamageableComponent>(target, out var damageablecomp))
_damageable.SetAllDamage(target, damageablecomp, 0); _damageable.SetAllDamage(target, damageablecomp, 0);
_mobState.ChangeMobState(target, MobState.Alive);
_mobThreshold.SetAllowRevives(target, false);
// Revive them now var factionComp = EnsureComp<NpcFactionMemberComponent>(target);
if (TryComp<MobStateComponent>(target, out var mobstate) && mobstate.CurrentState==MobState.Dead) foreach (var id in new List<string>(factionComp.Factions))
_mobState.ChangeMobState(target, MobState.Alive, mobstate); {
_faction.RemoveFaction(target, id);
}
_faction.AddFaction(target, "Zombie");
//gives it the funny "Zombie ___" name. //gives it the funny "Zombie ___" name.
var meta = MetaData(target); var meta = MetaData(target);
zombiecomp.BeforeZombifiedEntityName = meta.EntityName; 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); _identity.QueueIdentityUpdate(target);
//He's gotta have a mind //He's gotta have a mind
var mindComp = EnsureComp<MindContainerComponent>(target); 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 //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 //Greeting message for new bebe zombers
_chatMan.DispatchServerMessage(session, Loc.GetString("zombie-infection-greeting")); _chatMan.DispatchServerMessage(session, Loc.GetString("zombie-infection-greeting"));
// Notificate player about new role assignment // 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 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 //Goes through every hand, drops the items in it, then removes the hand
//may become the source of various bugs. //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); foreach (var hand in _hands.EnumerateHands(target))
_sharedHands.DoDrop(target, hand); {
_sharedHands.RemoveHand(target, hand.Name); _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: // No longer waiting to become a zombie:
// Requires deferral because this is (probably) the event which called ZombifyEntity in the first place. // Requires deferral because this is (probably) the event which called ZombifyEntity in the first place.
RemCompDeferred<PendingZombieComponent>(target); RemCompDeferred<PendingZombieComponent>(target);
//zombie gamemode stuff //zombie gamemode stuff
RaiseLocalEvent(new EntityZombifiedEvent(target)); var ev = new EntityZombifiedEvent(target);
RaiseLocalEvent(target, ref ev, true);
//zombies get slowdown once they convert //zombies get slowdown once they convert
_movementSpeedModifier.RefreshMovementSpeedModifiers(target); _movementSpeedModifier.RefreshMovementSpeedModifiers(target);
} }

View File

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

View File

@@ -399,12 +399,6 @@ namespace Content.Shared.CCVar
public static readonly CVarDef<int> ZombieMinPlayers = public static readonly CVarDef<int> ZombieMinPlayers =
CVarDef.Create("zombie.min_players", 20); 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 * Pirates
*/ */

View File

@@ -1,7 +1,9 @@
using Content.Shared.Chat.Prototypes; using Content.Shared.Chat.Prototypes;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Roles; using Content.Shared.Roles;
using Content.Shared.Humanoid; using Content.Shared.Humanoid;
using Content.Shared.StatusIcon;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
@@ -9,148 +11,135 @@ using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using static Content.Shared.Humanoid.HumanoidAppearanceState; using static Content.Shared.Humanoid.HumanoidAppearanceState;
namespace Content.Shared.Zombies namespace Content.Shared.Zombies;
[RegisterComponent, NetworkedComponent]
public sealed class ZombieComponent : Component
{ {
[RegisterComponent, NetworkedComponent] /// <summary>
public sealed class ZombieComponent : Component /// 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> DamageDict = new ()
/// 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 () { "Blunt", -0.4 },
{ { "Slash", -0.2 },
{ "Blunt", -0.4 }, { "Piercing", -0.2 }
{ "Slash", -0.2 }, }
{ "Piercing", -0.2 }, };
{ "Heat", -0.2 },
{ "Cold", -0.2 },
{ "Shock", -0.2 },
}
};
/// <summary> /// <summary>
/// Path to antagonist alert sound. /// A multiplier applied to <see cref="PassiveHealing"/> when the entity is in critical condition.
/// </summary> /// </summary>
[DataField("greetSoundNotification")] [DataField("passiveHealingCritMultiplier")]
public SoundSpecifier GreetSoundNotification = new SoundPathSpecifier("/Audio/Ambience/Antag/zombie_start.ogg"); public float PassiveHealingCritMultiplier = 2f;
/// <summary> /// <summary>
/// The blood reagent of the humanoid to restore in case of cloning /// Healing given when a zombie bites a living being.
/// </summary> /// </summary>
[DataField("beforeZombifiedBloodReagent")] [DataField("healingOnBite")]
public string BeforeZombifiedBloodReagent = String.Empty; public DamageSpecifier HealingOnBite = new()
{
DamageDict = new()
{
{ "Blunt", -2 },
{ "Slash", -2 },
{ "Piercing", -2 }
}
};
/// <summary> /// <summary>
/// The blood reagent to give the zombie. In case you want zombies that bleed milk, or something. /// Path to antagonist alert sound.
/// </summary> /// </summary>
[DataField("newBloodReagent")] [DataField("greetSoundNotification")]
public string NewBloodReagent = "ZombieBlood"; 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. /// Event that is broadcast whenever an entity is zombified.
/// Used by the zombie gamemode to track total infections. /// Used by the zombie gamemode to track total infections.
/// </summary> /// </summary>
[ByRefEvent]
public readonly struct EntityZombifiedEvent public readonly struct EntityZombifiedEvent
{ {
/// <summary> /// <summary>

View File

@@ -1,16 +1,18 @@
zombie-title = Zombies 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-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-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-healing = You feel a stirring in your flesh
zombie-infection-warning = You feel the zombie virus take hold zombie-infection-warning = You feel the zombie virus take hold
zombie-infection-underway = Your blood begins to thicken zombie-infection-underway = Your blood begins to thicken
zombie-alone = You feel entirely alone. 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 -> zombie-round-end-initial-count = {$initialCount ->
[one] There was one initial infected: [one] There was one initial infected:
*[other] There were {$initialCount} initial infected: *[other] There were {$initialCount} initial infected:

View File

@@ -1,9 +1,9 @@
zombie-transform = {CAPITALIZE(THE($target))} turned into a zombie! 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-generic = zombie
zombie-name-prefix = Zombified {$target} zombie-name-prefix = Zombified {$target}
zombie-role-desc = A malevolent creature of the dead. 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. zombie-permadeath = This time, you're dead for real.

View File

@@ -140,14 +140,14 @@
Poison: 0.8 Poison: 0.8
- type: damageModifierSet - 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: coefficients:
Blunt: 0.7 Blunt: 0.7
Slash: 1.1 Slash: 1.1
Piercing: 0.9 Piercing: 0.9
Shock: 1.5 Shock: 1.25
Cold: 0.3 Cold: 0.3
Heat: 2.0 Heat: 1.5
Poison: 0.0 Poison: 0.0
Radiation: 0.0 Radiation: 0.0

View File

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

View File

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

View File

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