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"),
public string PatientZeroPrototypeID = "InitialInfected";
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;
}
(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;
mind.AddRole(new TraitorRole(mind, _prototypeManager.Index<AntagPrototype>(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<PendingZombieComponent>(mind.OwnedEntity.Value);
+ 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;
{
DamageDict = new ()
{
- { "Blunt", 0.5 },
- { "Cellular", 0.2 },
+ { "Blunt", 0.8 },
{ "Toxin", 0.2 },
}
};
[DataField("nextTick", customTypeSerializer:typeof(TimeOffsetSerializer))]
public TimeSpan NextTick;
- // Scales damage over time.
- [DataField("infectedSecs")]
+ /// <summary>
+ /// Scales damage over time.
+ /// </summary>
+ [ViewVariables(VVAccess.ReadWrite), DataField("infectedSecs")]
public int InfectedSecs;
+
+ /// <summary>
+ /// Number of seconds that a typical infection will last before the player is totally overwhelmed with damage and
+ /// dies.
+ /// </summary>
+ [ViewVariables(VVAccess.ReadWrite), DataField("maxInfectionLength")]
+ public float MaxInfectionLength = 120f;
+
+ /// <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
+ /// </summary>
+ [DataField("infectionWarnings")]
+ public Dictionary<int, string> InfectionWarnings = new()
+ {
+ {-45, "zombie-infection-warning"},
+ {-30, "zombie-infection-warning"},
+ {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;
}
SubscribeLocalEvent<ZombieComponent, TryingToSleepEvent>(OnSleepAttempt);
SubscribeLocalEvent<PendingZombieComponent, MapInitEvent>(OnPendingMapInit);
+ SubscribeLocalEvent<PendingZombieComponent, MobStateChangedEvent>(OnPendingMobState);
}
private void OnPendingMapInit(EntityUid uid, PendingZombieComponent component, MapInitEvent args)
public override void Update(float frameTime)
{
base.Update(frameTime);
- var query = EntityQueryEnumerator<PendingZombieComponent, DamageableComponent>();
+ var query = EntityQueryEnumerator<PendingZombieComponent, DamageableComponent, MobStateComponent>();
var curTime = _timing.CurTime;
var zombQuery = EntityQueryEnumerator<ZombieComponent, DamageableComponent, MobStateComponent>();
// 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.NextTick = curTime;
+
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
- // 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);
+ // 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
}
}
+ 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;
{
if (_random.Prob(GetZombieInfectionChance(entity, component)))
{
- EnsureComp<PendingZombieComponent>(entity);
+ var pending = EnsureComp<PendingZombieComponent>(entity);
+ pending.MaxInfectionLength = _random.NextFloat(0.25f, 1.0f) * component.ZombieInfectionTurnTime;
EnsureComp<ZombifyOnDeathComponent>(entity);
}
}
}
RemComp<HandsComponent>(target);
// No longer waiting to become a zombie:
- RemComp<PendingZombieComponent>(target);
+ // 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));
[ViewVariables(VVAccess.ReadWrite)]
public float ZombieMovementSpeedDebuff = 0.75f;
+ /// <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 = 240.0f;
+
/// <summary>
/// The skin color of the zombie
/// </summary>
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.