From d3b6bb62c0bd80f5b5af9a2183a34ed40a5b2796 Mon Sep 17 00:00:00 2001 From: Tom Leys Date: Tue, 16 May 2023 17:59:39 +1200 Subject: [PATCH] Zombie virus delayed from 20-30 minutes from rule start. (#16346) --- .../Systems/AdminVerbSystem.Antags.cs | 6 +-- .../Rules/Components/ZombieRuleComponent.cs | 24 +++++++++ .../GameTicking/Rules/ZombieRuleSystem.cs | 10 +++- .../Zombies/PendingZombieComponent.cs | 37 ++++++++++++-- Content.Server/Zombies/ZombieSystem.cs | 50 ++++++++++++++++--- .../Zombies/ZombifyOnDeathSystem.cs | 3 +- Content.Shared/Zombies/ZombieComponent.cs | 7 +++ .../game-presets/preset-zombies.ftl | 2 + 8 files changed, 120 insertions(+), 19 deletions(-) diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs index 01caaa4e07..7b6ca14ea1 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs @@ -56,11 +56,7 @@ public sealed partial class AdminVerbSystem Icon = new SpriteSpecifier.Rsi(new("/Textures/Structures/Wallmounts/signs.rsi"), "bio"), Act = () => { - TryComp(args.Target, out MindComponent? mindComp); - if (mindComp == null || mindComp.Mind == null) - return; - - _zombify.ZombifyEntity(targetMindComp.Owner); + _zombify.ZombifyEntity(args.Target); }, Impact = LogImpact.High, Message = Loc.GetString("admin-verb-make-zombie"), diff --git a/Content.Server/GameTicking/Rules/Components/ZombieRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/ZombieRuleComponent.cs index 17d87a1ec5..869765c14f 100644 --- a/Content.Server/GameTicking/Rules/Components/ZombieRuleComponent.cs +++ b/Content.Server/GameTicking/Rules/Components/ZombieRuleComponent.cs @@ -8,4 +8,28 @@ public sealed class ZombieRuleComponent : Component public string PatientZeroPrototypeID = "InitialInfected"; public const string ZombifySelfActionPrototype = "TurnUndead"; + + /// + /// 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 + /// + [DataField("minZombieForceSecs"), ViewVariables(VVAccess.ReadWrite)] + public float MinZombieForceSecs = 1200; + + /// + /// After this many seconds the players will be forced to turn into zombies (at maximum) + /// Defaults to 30 minutes. 30*60 = 1800 seconds. + /// + [DataField("maxZombieForceSecs"), ViewVariables(VVAccess.ReadWrite)] + public float MaxZombieForceSecs = 1800; + + /// + /// How many additional seconds each player will get (at random) to scatter forced zombies over time. + /// Defaults to 2 minutes. 2*60 = 120 seconds. + /// + [DataField("playerZombieForceVariationSecs"), ViewVariables(VVAccess.ReadWrite)] + public float PlayerZombieForceVariationSecs = 120; } diff --git a/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs b/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs index a857c5c1c8..f7975e1560 100644 --- a/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs @@ -251,6 +251,10 @@ public sealed class ZombieRuleSystem : GameRuleSystem (int) Math.Min( Math.Floor((double) playerList.Count / playersPerInfected), maxInfected)); + // 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++) { IPlayerSession zombie; @@ -283,9 +287,13 @@ public sealed class ZombieRuleSystem : GameRuleSystem mind.AddRole(new TraitorRole(mind, _prototypeManager.Index(component.PatientZeroPrototypeID))); 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) { - EnsureComp(mind.OwnedEntity.Value); + var pending = EnsureComp(mind.OwnedEntity.Value); + // Only take damage after this many seconds + pending.InfectedSecs = -(int)(groupTimelimit + personalDelay); EnsureComp(mind.OwnedEntity.Value); inCharacterName = MetaData(mind.OwnedEntity.Value).EntityName; diff --git a/Content.Server/Zombies/PendingZombieComponent.cs b/Content.Server/Zombies/PendingZombieComponent.cs index 957e2a712a..83e86467f6 100644 --- a/Content.Server/Zombies/PendingZombieComponent.cs +++ b/Content.Server/Zombies/PendingZombieComponent.cs @@ -14,8 +14,7 @@ public sealed class PendingZombieComponent : Component { DamageDict = new () { - { "Blunt", 0.5 }, - { "Cellular", 0.2 }, + { "Blunt", 0.8 }, { "Toxin", 0.2 }, } }; @@ -23,7 +22,37 @@ public sealed class PendingZombieComponent : Component [DataField("nextTick", customTypeSerializer:typeof(TimeOffsetSerializer))] public TimeSpan NextTick; - // Scales damage over time. - [DataField("infectedSecs")] + /// + /// Scales damage over time. + /// + [ViewVariables(VVAccess.ReadWrite), DataField("infectedSecs")] public int InfectedSecs; + + /// + /// Number of seconds that a typical infection will last before the player is totally overwhelmed with damage and + /// dies. + /// + [ViewVariables(VVAccess.ReadWrite), DataField("maxInfectionLength")] + public float MaxInfectionLength = 120f; + + /// + /// 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 + /// + [DataField("infectionWarnings")] + public Dictionary InfectionWarnings = new() + { + {-45, "zombie-infection-warning"}, + {-30, "zombie-infection-warning"}, + {10, "zombie-infection-underway"}, + {25, "zombie-infection-underway"}, + }; + + /// + /// 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. + /// + [DataField("minimumCritMultiplier")] + public float MinimumCritMultiplier = 10; } diff --git a/Content.Server/Zombies/ZombieSystem.cs b/Content.Server/Zombies/ZombieSystem.cs index 8d5d0cd793..b051a7f89a 100644 --- a/Content.Server/Zombies/ZombieSystem.cs +++ b/Content.Server/Zombies/ZombieSystem.cs @@ -55,6 +55,7 @@ namespace Content.Server.Zombies SubscribeLocalEvent(OnSleepAttempt); SubscribeLocalEvent(OnPendingMapInit); + SubscribeLocalEvent(OnPendingMobState); } private void OnPendingMapInit(EntityUid uid, PendingZombieComponent component, MapInitEvent args) @@ -65,24 +66,47 @@ namespace Content.Server.Zombies public override void Update(float frameTime) { base.Update(frameTime); - var query = EntityQueryEnumerator(); + var query = EntityQueryEnumerator(); var curTime = _timing.CurTime; var zombQuery = EntityQueryEnumerator(); // Hurt the living infected - while (query.MoveNext(out var uid, out var comp, out var damage)) + 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) continue; - comp.InfectedSecs += 1; - // Pain of becoming a zombie grows over time - // 1x at 30s, 3x at 60s, 6x at 90s, 10x at 120s. - var pain_multiple = 0.1 + 0.02 * comp.InfectedSecs + 0.0005 * comp.InfectedSecs * comp.InfectedSecs; comp.NextTick = curTime; - _damageable.TryChangeDamage(uid, comp.Damage * pain_multiple, true, false, damage); + + 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. + 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; + + // 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); } // Heal the zombified @@ -168,6 +192,15 @@ namespace Content.Server.Zombies } } + 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); + } + } + private float GetZombieInfectionChance(EntityUid uid, ZombieComponent component) { var baseChance = component.MaxZombieInfectionChance; @@ -227,7 +260,8 @@ namespace Content.Server.Zombies { if (_random.Prob(GetZombieInfectionChance(entity, component))) { - EnsureComp(entity); + var pending = EnsureComp(entity); + pending.MaxInfectionLength = _random.NextFloat(0.25f, 1.0f) * component.ZombieInfectionTurnTime; EnsureComp(entity); } } diff --git a/Content.Server/Zombies/ZombifyOnDeathSystem.cs b/Content.Server/Zombies/ZombifyOnDeathSystem.cs index d77024efad..a0a26eb6e2 100644 --- a/Content.Server/Zombies/ZombifyOnDeathSystem.cs +++ b/Content.Server/Zombies/ZombifyOnDeathSystem.cs @@ -232,7 +232,8 @@ namespace Content.Server.Zombies } RemComp(target); // No longer waiting to become a zombie: - RemComp(target); + // Requires deferral because this is (probably) the event which called ZombifyEntity in the first place. + RemCompDeferred(target); //zombie gamemode stuff RaiseLocalEvent(new EntityZombifiedEvent(target)); diff --git a/Content.Shared/Zombies/ZombieComponent.cs b/Content.Shared/Zombies/ZombieComponent.cs index 87943531fc..1549d712c0 100644 --- a/Content.Shared/Zombies/ZombieComponent.cs +++ b/Content.Shared/Zombies/ZombieComponent.cs @@ -55,6 +55,13 @@ namespace Content.Shared.Zombies [ViewVariables(VVAccess.ReadWrite)] public float ZombieMovementSpeedDebuff = 0.75f; + /// + /// How long it takes our bite victims to turn in seconds (max). + /// Will roll 25% - 100% of this on bite. + /// + [DataField("zombieInfectionTurnTime"), ViewVariables(VVAccess.ReadWrite)] + public float ZombieInfectionTurnTime = 240.0f; + /// /// The skin color of the zombie /// diff --git a/Resources/Locale/en-US/game-ticking/game-presets/preset-zombies.ftl b/Resources/Locale/en-US/game-ticking/game-presets/preset-zombies.ftl index cef09786de..8c4e963ffa 100644 --- a/Resources/Locale/en-US/game-ticking/game-presets/preset-zombies.ftl +++ b/Resources/Locale/en-US/game-ticking/game-presets/preset-zombies.ftl @@ -6,6 +6,8 @@ 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-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.