Zombie virus delayed from 20-30 minutes from rule start. (#16346)

This commit is contained in:
Tom Leys
2023-05-16 17:59:39 +12:00
committed by GitHub
parent 10348fe11d
commit d3b6bb62c0
8 changed files with 120 additions and 19 deletions

View File

@@ -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"),

View File

@@ -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;
} }

View File

@@ -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;

View File

@@ -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;
} }

View File

@@ -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);
} }
} }

View File

@@ -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));

View File

@@ -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>

View File

@@ -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.