mirror of
https://github.com/space-syndicate/space-station-14.git
synced 2026-10-09 18:31:17 +03:00
* Solving #43723 by making zombies clumsy (they are unable to hold anything in their hands, passing items to them will fail). Also made zombies uncuff themselves whenever they transform (seeing as they no longer lose their hands meaning that cuffs would otherwise remain on them) * Cleaning up the code * Renamed ClumsyHoldStatusEffectComponent.cs to ClumsyGrabStatusEffectComponent.cs (and renamed associated files). I think "ClumsyGrab" works better than "ClumsyHold" (as the effect happens when the item is first grabbed, not whilst the item is being held). everything still works Reordered appropriate functions in ClumsyStatusTest.cs and ClumsyStatusEffectSystem.cs to retain alphabetical order. * oopsies :p * Appeased the YAMLLinter (at the cost of a somewhat hackier implementation). ZombieSystem.Transform.cs now applies the zombie clumsiness via creating a new PermanentStatusEffectComponent with StatusEffectClumsyZombie as its status effect. * requested changes have been made (also re-added newline to end of clumsy-component.ftl because it has a newline at the end of it on the master branch, I guess it's probably supposed to be there) * reordering so private members (including new one) are before the internal member * hands are forcibly emptied - even if something's glued to them. * UnZombify should remove the lingering ZombieClumsy status effect (still needs to revert the rest of the zombification process, but at least the new bit I added can be undone) * Minor refactor of my own refactor. New order of operations: 1. Uncuffs zombie (if needed) 2. Attempts to `DropAll` items from hands 3. Iterates through hand containers to empty them (removes otherwise unremovable items from hands) Will refactor process to force drop all items if #45844 becomes implementable + is merged. * formatting * how the hell did the metabolizer code go missing!? * uncuffing now happens in a safer way less likely to accidentally infinitely loop * tweaking that one comment I was asked to tweak (this is unrelated to the hand malarkey) hopefully it won't lead to any merge conflicts later on * formatting * file style --------- Co-authored-by: Pok245 <113675512+Pok27@users.noreply.github.com>
361 lines
15 KiB
C#
361 lines
15 KiB
C#
using Content.Shared.NPC.Prototypes;
|
|
using Content.Server.Actions;
|
|
using Content.Server.Body.Systems;
|
|
using Content.Server.Chat;
|
|
using Content.Server.Chat.Systems;
|
|
using Content.Server.Emoting.Systems;
|
|
using Content.Shared.Speech.EntitySystems;
|
|
using Content.Shared.Anomaly.Components;
|
|
using Content.Shared.Armor;
|
|
using Content.Shared.Bed.Sleep;
|
|
using Content.Shared.Body.Systems;
|
|
using Content.Shared.Cloning.Events;
|
|
using Content.Shared.Chat;
|
|
using Content.Shared.Damage.Systems;
|
|
using Content.Shared.Inventory;
|
|
using Content.Shared.Mind;
|
|
using Content.Shared.Mind.Components;
|
|
using Content.Shared.Mobs;
|
|
using Content.Shared.Mobs.Components;
|
|
using Content.Shared.Mobs.Systems;
|
|
using Content.Shared.Popups;
|
|
using Content.Shared.Revolutionary;
|
|
using Content.Shared.Roles;
|
|
using Content.Shared.Roles.Components;
|
|
using Content.Shared.Weapons.Melee.Events;
|
|
using Content.Shared.Zombies;
|
|
using Robust.Shared.Prototypes;
|
|
using Robust.Shared.Random;
|
|
using Robust.Shared.Timing;
|
|
using Content.Server.Ghost.Roles.Components;
|
|
using Content.Shared.Medical;
|
|
using Content.Shared.Construction.Steps;
|
|
|
|
namespace Content.Server.Zombies
|
|
{
|
|
public sealed partial class ZombieSystem : SharedZombieSystem
|
|
{
|
|
[Dependency] private IGameTiming _timing = default!;
|
|
[Dependency] private IRobustRandom _random = default!;
|
|
[Dependency] private BloodstreamSystem _bloodstream = default!;
|
|
[Dependency] private DamageableSystem _damageable = default!;
|
|
[Dependency] private ChatSystem _chat = default!;
|
|
[Dependency] private ActionsSystem _actions = default!;
|
|
[Dependency] private AutoEmoteSystem _autoEmote = default!;
|
|
[Dependency] private EmoteOnDamageSystem _emoteOnDamage = default!;
|
|
[Dependency] private MobStateSystem _mobState = default!;
|
|
[Dependency] private SharedPopupSystem _popup = default!;
|
|
[Dependency] private SharedRoleSystem _role = default!;
|
|
|
|
public readonly ProtoId<NpcFactionPrototype> Faction = "Zombie";
|
|
|
|
public const SlotFlags ProtectiveSlots =
|
|
SlotFlags.FEET |
|
|
SlotFlags.HEAD |
|
|
SlotFlags.EYES |
|
|
SlotFlags.GLOVES |
|
|
SlotFlags.MASK |
|
|
SlotFlags.NECK |
|
|
SlotFlags.INNERCLOTHING |
|
|
SlotFlags.OUTERCLOTHING;
|
|
|
|
public override void Initialize()
|
|
{
|
|
base.Initialize();
|
|
|
|
SubscribeLocalEvent<ZombieComponent, EmoteEvent>(OnEmote, before:
|
|
new[] { typeof(VocalSystem), typeof(BodyEmotesSystem) });
|
|
|
|
SubscribeLocalEvent<ZombieComponent, MeleeHitEvent>(OnMeleeHit);
|
|
SubscribeLocalEvent<ZombieComponent, MobStateChangedEvent>(OnMobState);
|
|
SubscribeLocalEvent<ZombieComponent, CloningEvent>(OnZombieCloning);
|
|
SubscribeLocalEvent<ZombieComponent, TryingToSleepEvent>(OnSleepAttempt);
|
|
SubscribeLocalEvent<ZombieComponent, GetCharactedDeadIcEvent>(OnGetCharacterDeadIC);
|
|
SubscribeLocalEvent<ZombieComponent, GetCharacterUnrevivableIcEvent>(OnGetCharacterUnrevivableIC);
|
|
SubscribeLocalEvent<ZombieComponent, MindAddedMessage>(OnMindAdded);
|
|
SubscribeLocalEvent<ZombieComponent, MindRemovedMessage>(OnMindRemoved);
|
|
SubscribeLocalEvent<ZombieComponent, AttemptConvertRevolutionaryEvent>(OnAttemptConvert);
|
|
|
|
SubscribeLocalEvent<PendingZombieComponent, MapInitEvent>(OnPendingMapInit);
|
|
SubscribeLocalEvent<PendingZombieComponent, BeforeRemoveAnomalyOnDeathEvent>(OnBeforeRemoveAnomalyOnDeath);
|
|
|
|
SubscribeLocalEvent<IncurableZombieComponent, MapInitEvent>(OnPendingMapInit);
|
|
|
|
SubscribeLocalEvent<ZombifyOnDeathComponent, MobStateChangedEvent>(OnDamageChanged);
|
|
}
|
|
|
|
private void OnBeforeRemoveAnomalyOnDeath(Entity<PendingZombieComponent> ent, ref BeforeRemoveAnomalyOnDeathEvent args)
|
|
{
|
|
// Pending zombies (e.g. infected non-zombies) do not remove their hosted anomaly on death.
|
|
// Current zombies DO remove the anomaly on death.
|
|
args.Cancelled = true;
|
|
}
|
|
|
|
private void OnPendingMapInit(EntityUid uid, IncurableZombieComponent component, MapInitEvent args)
|
|
{
|
|
_actions.AddAction(uid, ref component.Action, component.ZombifySelfActionPrototype);
|
|
_faction.AddFaction(uid, Faction);
|
|
|
|
if (HasComp<ZombieComponent>(uid) || HasComp<ZombieImmuneComponent>(uid))
|
|
return;
|
|
|
|
EnsureComp<PendingZombieComponent>(uid, out PendingZombieComponent pendingComp);
|
|
|
|
pendingComp.GracePeriod = _random.Next(pendingComp.MinInitialInfectedGrace, pendingComp.MaxInitialInfectedGrace);
|
|
}
|
|
|
|
private void OnPendingMapInit(EntityUid uid, PendingZombieComponent component, MapInitEvent args)
|
|
{
|
|
if (_mobState.IsDead(uid))
|
|
{
|
|
ZombifyEntity(uid);
|
|
return;
|
|
}
|
|
|
|
component.NextTick = _timing.CurTime + TimeSpan.FromSeconds(1f);
|
|
}
|
|
|
|
public override void Update(float frameTime)
|
|
{
|
|
base.Update(frameTime);
|
|
var curTime = _timing.CurTime;
|
|
|
|
// Hurt the living infected
|
|
var query = EntityQueryEnumerator<PendingZombieComponent, Shared.Damage.Components.DamageableComponent, MobStateComponent>();
|
|
while (query.MoveNext(out var uid, out var comp, out var damage, out var mobState))
|
|
{
|
|
// Process only once per second
|
|
if (comp.NextTick > curTime)
|
|
continue;
|
|
|
|
comp.NextTick = curTime + TimeSpan.FromSeconds(1f);
|
|
|
|
comp.GracePeriod -= TimeSpan.FromSeconds(1f);
|
|
if (comp.GracePeriod > TimeSpan.Zero)
|
|
continue;
|
|
|
|
if (_random.Prob(comp.InfectionWarningChance))
|
|
_popup.PopupEntity(Loc.GetString(_random.Pick(comp.InfectionWarnings)), uid, uid);
|
|
|
|
var multiplier = _mobState.IsCritical(uid, mobState)
|
|
? comp.CritDamageMultiplier
|
|
: 1f;
|
|
|
|
_damageable.ChangeDamage((uid, damage), comp.Damage * multiplier, true, false);
|
|
}
|
|
|
|
// Heal the zombified
|
|
var zombQuery = EntityQueryEnumerator<ZombieComponent, Shared.Damage.Components.DamageableComponent, MobStateComponent>();
|
|
while (zombQuery.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;
|
|
|
|
if (_mobState.IsDead(uid, mobState))
|
|
continue;
|
|
|
|
var multiplier = _mobState.IsCritical(uid, mobState)
|
|
? comp.PassiveHealingCritMultiplier
|
|
: 1f;
|
|
|
|
// Gradual healing for living zombies.
|
|
_damageable.ChangeDamage((uid, damage), comp.PassiveHealing * multiplier, true, false);
|
|
}
|
|
}
|
|
|
|
private void OnSleepAttempt(EntityUid uid, ZombieComponent component, ref TryingToSleepEvent args)
|
|
{
|
|
args.Cancelled = true;
|
|
}
|
|
|
|
private void OnGetCharacterDeadIC(EntityUid uid, ZombieComponent component, ref GetCharactedDeadIcEvent args)
|
|
{
|
|
args.Dead = true;
|
|
}
|
|
|
|
private void OnGetCharacterUnrevivableIC(EntityUid uid, ZombieComponent component, ref GetCharacterUnrevivableIcEvent args)
|
|
{
|
|
args.Unrevivable = true;
|
|
}
|
|
|
|
private void OnEmote(EntityUid uid, ZombieComponent component, ref EmoteEvent args)
|
|
{
|
|
// always play zombie emote sounds and ignore others
|
|
if (args.Handled)
|
|
return;
|
|
|
|
ProtoMan.Resolve(component.EmoteSoundsId, out var sounds);
|
|
|
|
args.Handled = _chat.TryPlayEmoteSound(uid, sounds, args.Emote);
|
|
}
|
|
|
|
private void OnMobState(EntityUid uid, ZombieComponent component, MobStateChangedEvent args)
|
|
{
|
|
if (args.NewMobState == MobState.Alive)
|
|
{
|
|
// Groaning when damaged
|
|
EnsureComp<EmoteOnDamageComponent>(uid);
|
|
_emoteOnDamage.AddEmote(uid, "Scream");
|
|
|
|
// Random groaning
|
|
EnsureComp<AutoEmoteComponent>(uid);
|
|
_autoEmote.AddEmote(uid, "ZombieGroan");
|
|
}
|
|
else
|
|
{
|
|
// Stop groaning when damaged
|
|
_emoteOnDamage.RemoveEmote(uid, "Scream");
|
|
|
|
// Stop random groaning
|
|
_autoEmote.RemoveEmote(uid, "ZombieGroan");
|
|
}
|
|
}
|
|
|
|
private float GetZombieInfectionChance(EntityUid uid, ZombieComponent zombieComponent)
|
|
{
|
|
var chance = zombieComponent.BaseZombieInfectionChance;
|
|
|
|
var armorEv = new CoefficientQueryEvent(ProtectiveSlots);
|
|
RaiseLocalEvent(uid, armorEv);
|
|
foreach (var resistanceEffectiveness in zombieComponent.ResistanceEffectiveness.DamageDict)
|
|
{
|
|
if (armorEv.DamageModifiers.Coefficients.TryGetValue(resistanceEffectiveness.Key, out var coefficient))
|
|
{
|
|
// Scale the coefficient by the resistance effectiveness, very descriptive I know
|
|
// For example. With 30% slash resist (0.7 coeff), but only a 60% resistance effectiveness for slash,
|
|
// you'll end up with 1 - (0.3 * 0.6) = 0.82 coefficient, or a 18% resistance
|
|
var adjustedCoefficient = 1 - ((1 - coefficient) * resistanceEffectiveness.Value.Float());
|
|
chance *= adjustedCoefficient;
|
|
}
|
|
}
|
|
|
|
var zombificationResistanceEv = new ZombificationResistanceQueryEvent(ProtectiveSlots);
|
|
RaiseLocalEvent(uid, zombificationResistanceEv);
|
|
chance *= zombificationResistanceEv.TotalCoefficient;
|
|
|
|
return MathF.Max(chance, zombieComponent.MinZombieInfectionChance);
|
|
}
|
|
|
|
private void OnMeleeHit(Entity<ZombieComponent> entity, ref MeleeHitEvent args)
|
|
{
|
|
if (!args.IsHit)
|
|
return;
|
|
|
|
var cannotSpread = HasComp<NonSpreaderZombieComponent>(args.User);
|
|
|
|
foreach (var uid in args.HitEntities)
|
|
{
|
|
if (args.User == uid)
|
|
continue;
|
|
|
|
if (!TryComp<MobStateComponent>(uid, out var mobState))
|
|
continue;
|
|
|
|
if (HasComp<ZombieComponent>(uid) || HasComp<IncurableZombieComponent>(uid))
|
|
{
|
|
// Don't infect, don't deal damage, do not heal from bites, don't pass go!
|
|
args.Handled = true;
|
|
continue;
|
|
}
|
|
|
|
if (_mobState.IsAlive(uid, mobState))
|
|
{
|
|
_damageable.TryChangeDamage(args.User, entity.Comp.HealingOnBite, true, false);
|
|
|
|
// If we cannot infect the living target, the zed will just heal itself.
|
|
if (HasComp<ZombieImmuneComponent>(uid) || cannotSpread || !_random.Prob(GetZombieInfectionChance(uid, entity.Comp)))
|
|
continue;
|
|
|
|
EnsureComp<PendingZombieComponent>(uid);
|
|
EnsureComp<ZombifyOnDeathComponent>(uid);
|
|
}
|
|
else
|
|
{
|
|
if (HasComp<ZombieImmuneComponent>(uid) || cannotSpread)
|
|
continue;
|
|
|
|
// If the target is dead and can be infected, infect.
|
|
ZombifyEntity(uid);
|
|
args.Handled = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// This is the function to call if you want to unzombify an entity.
|
|
/// </summary>
|
|
/// <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="zombiecomp"></param>
|
|
/// <remarks>
|
|
/// this currently only restores the skin/eye color from before zombified + removes clumsy zombie status effect
|
|
/// TODO: completely rethink how zombies are done to allow reversal.
|
|
/// TODO: maybe move to ZombieSystem.Transform.cs?
|
|
/// </remarks>
|
|
public bool UnZombify(EntityUid source, EntityUid target, ZombieComponent? zombiecomp)
|
|
{
|
|
if (!Resolve(source, ref zombiecomp))
|
|
return false;
|
|
|
|
_visualBody.ApplyProfiles(target, zombiecomp.BeforeZombifiedProfiles);
|
|
_visualBody.ApplyMarkings(target, zombiecomp.BeforeZombifiedMarkings);
|
|
|
|
_bloodstream.ChangeBloodReagents(target, zombiecomp.BeforeZombifiedBloodReagents);
|
|
|
|
// Restore the blood refresh amount to what it was before zombification. They can't regain blood otherwise.
|
|
_bloodstream.ChangeBloodRefreshAmount(target, zombiecomp.BeforeZombifiedBloodRefresh);
|
|
_bloodstream.ChangeBloodIncreaseEnabled(target, true);
|
|
|
|
// no longer a zombie, no longer *that* clumsy
|
|
_statusEffects.TryRemoveStatusEffect(target, ClumsyZombieStatus);
|
|
|
|
// Remove the tags that we added during Zombification
|
|
_tag.RemoveTag(target, CannotSuicideTag);
|
|
_tag.RemoveTag(target, InvalidForGlobalSpawnSpellTag);
|
|
return true;
|
|
}
|
|
|
|
private void OnZombieCloning(Entity<ZombieComponent> ent, ref CloningEvent args)
|
|
{
|
|
UnZombify(ent.Owner, args.CloneUid, ent.Comp);
|
|
}
|
|
|
|
// Make sure players that enter a zombie (for example via a ghost role or the mind swap spell) count as an antagonist.
|
|
private void OnMindAdded(Entity<ZombieComponent> ent, ref MindAddedMessage args)
|
|
{
|
|
if (!_role.MindHasRole<ZombieRoleComponent>(args.Mind))
|
|
_role.MindAddRole(args.Mind, "MindRoleZombie", mind: args.Mind.Comp);
|
|
}
|
|
|
|
// Remove the role when getting cloned, getting gibbed and borged, or leaving the body via any other method.
|
|
// We also need to make sure the zombie is a ghost role because zombies with minds do not get a ghostrolecomponent
|
|
private void OnMindRemoved(Entity<ZombieComponent> ent, ref MindRemovedMessage args)
|
|
{
|
|
_role.MindRemoveRole<ZombieRoleComponent>((args.Mind.Owner, args.Mind.Comp));
|
|
MakeGhostRole(ent.Owner);
|
|
}
|
|
|
|
private void OnAttemptConvert(Entity<ZombieComponent> ent, ref AttemptConvertRevolutionaryEvent args)
|
|
{
|
|
args.Cancelled = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Makes the target entity a zombie ghost role. Should only be fired when the entity does not have a mind.
|
|
/// </summary>
|
|
private void MakeGhostRole(EntityUid ent)
|
|
{
|
|
//yet more hardcoding. Visit zombie.ftl for more information.
|
|
var ghostRole = EnsureComp<GhostRoleComponent>(ent);
|
|
EnsureComp<GhostTakeoverAvailableComponent>(ent);
|
|
|
|
ghostRole.RoleName = Loc.GetString("zombie-generic");
|
|
ghostRole.RoleDescription = Loc.GetString("zombie-role-desc");
|
|
ghostRole.RoleRules = Loc.GetString("zombie-role-rules");
|
|
ghostRole.MindRoles.Add(MindRoleZombie);
|
|
}
|
|
}
|
|
}
|