Zombie virus delayed from 20-30 minutes from rule start. (#16346)
This commit is contained in:
@@ -56,11 +56,7 @@ public sealed partial class AdminVerbSystem
|
|||||||
Icon = new SpriteSpecifier.Rsi(new("/Textures/Structures/Wallmounts/signs.rsi"), "bio"),
|
Icon = new SpriteSpecifier.Rsi(new("/Textures/Structures/Wallmounts/signs.rsi"), "bio"),
|
||||||
Act = () =>
|
Act = () =>
|
||||||
{
|
{
|
||||||
TryComp(args.Target, out MindComponent? mindComp);
|
_zombify.ZombifyEntity(args.Target);
|
||||||
if (mindComp == null || mindComp.Mind == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_zombify.ZombifyEntity(targetMindComp.Owner);
|
|
||||||
},
|
},
|
||||||
Impact = LogImpact.High,
|
Impact = LogImpact.High,
|
||||||
Message = Loc.GetString("admin-verb-make-zombie"),
|
Message = Loc.GetString("admin-verb-make-zombie"),
|
||||||
|
|||||||
@@ -8,4 +8,28 @@ public sealed class ZombieRuleComponent : Component
|
|||||||
|
|
||||||
public string PatientZeroPrototypeID = "InitialInfected";
|
public string PatientZeroPrototypeID = "InitialInfected";
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -251,6 +251,10 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
|
|||||||
(int) Math.Min(
|
(int) Math.Min(
|
||||||
Math.Floor((double) playerList.Count / playersPerInfected), maxInfected));
|
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++)
|
for (var i = 0; i < numInfected; i++)
|
||||||
{
|
{
|
||||||
IPlayerSession zombie;
|
IPlayerSession zombie;
|
||||||
@@ -283,9 +287,13 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
|
|||||||
mind.AddRole(new TraitorRole(mind, _prototypeManager.Index<AntagPrototype>(component.PatientZeroPrototypeID)));
|
mind.AddRole(new TraitorRole(mind, _prototypeManager.Index<AntagPrototype>(component.PatientZeroPrototypeID)));
|
||||||
|
|
||||||
var inCharacterName = string.Empty;
|
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)
|
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);
|
EnsureComp<ZombifyOnDeathComponent>(mind.OwnedEntity.Value);
|
||||||
inCharacterName = MetaData(mind.OwnedEntity.Value).EntityName;
|
inCharacterName = MetaData(mind.OwnedEntity.Value).EntityName;
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ public sealed class PendingZombieComponent : Component
|
|||||||
{
|
{
|
||||||
DamageDict = new ()
|
DamageDict = new ()
|
||||||
{
|
{
|
||||||
{ "Blunt", 0.5 },
|
{ "Blunt", 0.8 },
|
||||||
{ "Cellular", 0.2 },
|
|
||||||
{ "Toxin", 0.2 },
|
{ "Toxin", 0.2 },
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -23,7 +22,37 @@ public sealed class PendingZombieComponent : Component
|
|||||||
[DataField("nextTick", customTypeSerializer:typeof(TimeOffsetSerializer))]
|
[DataField("nextTick", customTypeSerializer:typeof(TimeOffsetSerializer))]
|
||||||
public TimeSpan NextTick;
|
public TimeSpan NextTick;
|
||||||
|
|
||||||
// Scales damage over time.
|
/// <summary>
|
||||||
[DataField("infectedSecs")]
|
/// Scales damage over time.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables(VVAccess.ReadWrite), DataField("infectedSecs")]
|
||||||
public int 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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ namespace Content.Server.Zombies
|
|||||||
SubscribeLocalEvent<ZombieComponent, TryingToSleepEvent>(OnSleepAttempt);
|
SubscribeLocalEvent<ZombieComponent, TryingToSleepEvent>(OnSleepAttempt);
|
||||||
|
|
||||||
SubscribeLocalEvent<PendingZombieComponent, MapInitEvent>(OnPendingMapInit);
|
SubscribeLocalEvent<PendingZombieComponent, MapInitEvent>(OnPendingMapInit);
|
||||||
|
SubscribeLocalEvent<PendingZombieComponent, MobStateChangedEvent>(OnPendingMobState);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPendingMapInit(EntityUid uid, PendingZombieComponent component, MapInitEvent args)
|
private void OnPendingMapInit(EntityUid uid, PendingZombieComponent component, MapInitEvent args)
|
||||||
@@ -65,24 +66,47 @@ namespace Content.Server.Zombies
|
|||||||
public override void Update(float frameTime)
|
public override void Update(float frameTime)
|
||||||
{
|
{
|
||||||
base.Update(frameTime);
|
base.Update(frameTime);
|
||||||
var query = EntityQueryEnumerator<PendingZombieComponent, DamageableComponent>();
|
var query = EntityQueryEnumerator<PendingZombieComponent, DamageableComponent, MobStateComponent>();
|
||||||
var curTime = _timing.CurTime;
|
var curTime = _timing.CurTime;
|
||||||
|
|
||||||
var zombQuery = EntityQueryEnumerator<ZombieComponent, DamageableComponent, MobStateComponent>();
|
var zombQuery = EntityQueryEnumerator<ZombieComponent, DamageableComponent, MobStateComponent>();
|
||||||
|
|
||||||
// Hurt the living infected
|
// 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
|
// Process only once per second
|
||||||
if (comp.NextTick + TimeSpan.FromSeconds(1) > curTime)
|
if (comp.NextTick + TimeSpan.FromSeconds(1) > curTime)
|
||||||
continue;
|
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;
|
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
|
// 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)
|
private float GetZombieInfectionChance(EntityUid uid, ZombieComponent component)
|
||||||
{
|
{
|
||||||
var baseChance = component.MaxZombieInfectionChance;
|
var baseChance = component.MaxZombieInfectionChance;
|
||||||
@@ -227,7 +260,8 @@ namespace Content.Server.Zombies
|
|||||||
{
|
{
|
||||||
if (_random.Prob(GetZombieInfectionChance(entity, component)))
|
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);
|
EnsureComp<ZombifyOnDeathComponent>(entity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -232,7 +232,8 @@ namespace Content.Server.Zombies
|
|||||||
}
|
}
|
||||||
RemComp<HandsComponent>(target);
|
RemComp<HandsComponent>(target);
|
||||||
// No longer waiting to become a zombie:
|
// 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
|
//zombie gamemode stuff
|
||||||
RaiseLocalEvent(new EntityZombifiedEvent(target));
|
RaiseLocalEvent(new EntityZombifiedEvent(target));
|
||||||
|
|||||||
@@ -55,6 +55,13 @@ namespace Content.Shared.Zombies
|
|||||||
[ViewVariables(VVAccess.ReadWrite)]
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
public float ZombieMovementSpeedDebuff = 0.75f;
|
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>
|
/// <summary>
|
||||||
/// The skin color of the zombie
|
/// The skin color of the zombie
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -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-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-healing = You feel a stirring in your flesh
|
||||||
|
zombie-infection-warning = You feel the zombie virus take hold
|
||||||
|
zombie-infection-underway = Your blood begins to thicken
|
||||||
|
|
||||||
zombie-alone = You feel entirely alone.
|
zombie-alone = You feel entirely alone.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user