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>
375 lines
16 KiB
C#
375 lines
16 KiB
C#
using System.Linq;
|
|
using Content.Server.Administration.Managers;
|
|
using Content.Server.Atmos.Components;
|
|
using Content.Server.Body.Components;
|
|
using Content.Server.Chat;
|
|
using Content.Server.Chat.Managers;
|
|
using Content.Server.Ghost;
|
|
using Content.Server.Inventory;
|
|
using Content.Server.Mind;
|
|
using Content.Server.NPC;
|
|
using Content.Server.NPC.HTN;
|
|
using Content.Server.NPC.Systems;
|
|
using Content.Server.StationEvents.Components;
|
|
using Content.Shared.Body;
|
|
using Content.Shared.Body.Components;
|
|
using Content.Shared.CombatMode;
|
|
using Content.Shared.CombatMode.Pacification;
|
|
using Content.Shared.Cuffs;
|
|
using Content.Shared.Ghost.Roles.Components;
|
|
using Content.Shared.Hands.EntitySystems;
|
|
using Content.Shared.Humanoid;
|
|
using Content.Shared.IdentityManagement;
|
|
using Content.Shared.Interaction.Components;
|
|
using Content.Shared.Metabolism;
|
|
using Content.Shared.Mobs;
|
|
using Content.Shared.Mobs.Components;
|
|
using Content.Shared.Movement.Pulling.Components;
|
|
using Content.Shared.Movement.Systems;
|
|
using Content.Shared.NameModifier.EntitySystems;
|
|
using Content.Shared.NPC.Prototypes;
|
|
using Content.Shared.NPC.Systems;
|
|
using Content.Shared.Nutrition.AnimalHusbandry;
|
|
using Content.Shared.Nutrition.Components;
|
|
using Content.Shared.Popups;
|
|
using Content.Shared.Prying.Components;
|
|
using Content.Shared.Roles;
|
|
using Content.Shared.Speech.EntitySystems;
|
|
using Content.Shared.StatusEffectNew;
|
|
using Content.Shared.StatusEffectNew.Components;
|
|
using Content.Shared.Tag;
|
|
using Content.Shared.Temperature.Components;
|
|
using Content.Shared.Traits.Assorted;
|
|
using Content.Shared.Weapons.Melee;
|
|
using Content.Shared.Zombies;
|
|
using Robust.Shared.Audio.Systems;
|
|
using Robust.Shared.Containers;
|
|
using Robust.Shared.Player;
|
|
using Robust.Shared.Prototypes;
|
|
using Robust.Shared.Utility;
|
|
|
|
namespace Content.Server.Zombies;
|
|
|
|
/// <summary>
|
|
/// Handles zombie propagation and inherent zombie traits
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Don't Shitcode Open Inside
|
|
/// </remarks>
|
|
public sealed partial class ZombieSystem
|
|
{
|
|
[Dependency] private SharedAudioSystem _audio = default!;
|
|
[Dependency] private IBanManager _ban = default!;
|
|
[Dependency] private IChatManager _chatMan = default!;
|
|
[Dependency] private SharedCombatModeSystem _combat = default!;
|
|
[Dependency] private NpcFactionSystem _faction = default!;
|
|
[Dependency] private GhostSystem _ghost = default!;
|
|
[Dependency] private SharedHandsSystem _hands = default!;
|
|
[Dependency] private SharedVisualBodySystem _visualBody = default!;
|
|
[Dependency] private IdentitySystem _identity = default!;
|
|
[Dependency] private ServerInventorySystem _inventory = default!;
|
|
[Dependency] private MindSystem _mind = default!;
|
|
[Dependency] private MovementSpeedModifierSystem _movementSpeedModifier = default!;
|
|
[Dependency] private NameModifierSystem _nameMod = default!;
|
|
[Dependency] private ReplacementAccentSystem _replacementAccent = default!;
|
|
[Dependency] private NPCSystem _npc = default!;
|
|
[Dependency] private TagSystem _tag = default!;
|
|
[Dependency] private ISharedPlayerManager _player = default!;
|
|
[Dependency] private BodySystem _body = default!;
|
|
[Dependency] private SharedContainerSystem _containerSystem = default!;
|
|
[Dependency] private StatusEffectsSystem _statusEffects = default!;
|
|
[Dependency] private SharedCuffableSystem _cuffable = default!;
|
|
|
|
private static readonly ProtoId<TagPrototype> InvalidForGlobalSpawnSpellTag = "InvalidForGlobalSpawnSpell";
|
|
private static readonly ProtoId<TagPrototype> CannotSuicideTag = "CannotSuicide";
|
|
private static readonly ProtoId<NpcFactionPrototype> ZombieFaction = "Zombie";
|
|
private static readonly string MindRoleZombie = "MindRoleZombie";
|
|
private static readonly List<ProtoId<AntagPrototype>> BannableZombiePrototypes = ["Zombie"];
|
|
private static readonly EntProtoId<StatusEffectComponent> ClumsyZombieStatus = "StatusEffectClumsyZombie";
|
|
internal static readonly HashSet<HumanoidVisualLayers> AdditionalZombieLayers = [HumanoidVisualLayers.Tail, HumanoidVisualLayers.HeadSide, HumanoidVisualLayers.HeadTop, HumanoidVisualLayers.Snout];
|
|
|
|
/// <summary>
|
|
/// Handles an entity turning into a zombie when they die or go into crit
|
|
/// </summary>
|
|
private void OnDamageChanged(EntityUid uid, ZombifyOnDeathComponent component, MobStateChangedEvent args)
|
|
{
|
|
if (args.NewMobState == MobState.Dead)
|
|
{
|
|
ZombifyEntity(uid, args.Component);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// This is the general purpose function to call if you want to zombify an entity.
|
|
/// It handles both humanoid and nonhumanoid transformation and everything should be called through it.
|
|
/// </summary>
|
|
/// <param name="target">the entity being zombified</param>
|
|
/// <param name="mobState"></param>
|
|
/// <remarks>
|
|
/// ALRIGHT BIG BOYS, GIRLS AND ANYONE ELSE. YOU'VE COME TO THE LAYER OF THE BEAST. THIS IS YOUR WARNING.
|
|
/// This function is the god function for zombie stuff, and it is cursed. I have
|
|
/// attempted to label everything thouroughly for your sanity. I have attempted to
|
|
/// rewrite this, but this is how it shall lie eternal. Turn back now.
|
|
/// -emo
|
|
/// </remarks>
|
|
public void ZombifyEntity(EntityUid target, MobStateComponent? mobState = null)
|
|
{
|
|
//Don't zombfiy zombies
|
|
if (HasComp<ZombieComponent>(target) || HasComp<ZombieImmuneComponent>(target))
|
|
return;
|
|
|
|
if (!Resolve(target, ref mobState, logMissing: false))
|
|
return;
|
|
|
|
// Detach role-banned players before zombification
|
|
if (TryComp<ActorComponent>(target, out var actor) && _ban.IsRoleBanned(actor.PlayerSession, BannableZombiePrototypes))
|
|
{
|
|
var sess = actor.PlayerSession;
|
|
var message = Loc.GetString("zombie-roleban-ghosted");
|
|
|
|
if (_mind.TryGetMind(sess, out var playerMindEnt, out var playerMind))
|
|
{
|
|
// Detach
|
|
_ghost.SpawnGhost((playerMindEnt, playerMind), target);
|
|
|
|
// Notify
|
|
_chatMan.DispatchServerMessage(sess, message);
|
|
}
|
|
else
|
|
Log.Error($"Mind for session '{sess}' could not be found");
|
|
}
|
|
|
|
//you're a real zombie now, son.
|
|
var zombiecomp = AddComp<ZombieComponent>(target);
|
|
|
|
//we need to basically remove all of these because zombies shouldn't
|
|
//get diseases, breath, be thirst, be hungry, die in space, get double sentience, have offspring or be paraplegic.
|
|
RemComp<RespiratorComponent>(target);
|
|
RemComp<BarotraumaComponent>(target);
|
|
RemComp<SatiationComponent>(target);
|
|
RemComp<ReproductiveComponent>(target);
|
|
RemComp<ReproductivePartnerComponent>(target);
|
|
RemComp<LegsParalyzedComponent>(target);
|
|
RemComp<ComplexInteractionComponent>(target);
|
|
RemComp<SentienceTargetComponent>(target);
|
|
|
|
// remove the metabolizer from all the body's organs. they're an undead.
|
|
var metabolizerOrgans = _body.EnumerateOrgans<MetabolizerComponent>(target);
|
|
foreach(var organ in metabolizerOrgans)
|
|
{
|
|
RemComp<MetabolizerComponent>(organ);
|
|
}
|
|
|
|
//funny voice
|
|
var accentType = "zombie";
|
|
if (TryComp<ZombieAccentOverrideComponent>(target, out var accent))
|
|
accentType = accent.Accent;
|
|
|
|
_replacementAccent.ApplyAccent(target, accentType);
|
|
|
|
//This is needed for stupid entities that fuck up combat mode component
|
|
//in an attempt to make an entity not attack. This is the easiest way to do it.
|
|
var combat = EnsureComp<CombatModeComponent>(target);
|
|
RemComp<PacifiedComponent>(target);
|
|
_combat.SetCanDisarm(target, false, combat);
|
|
|
|
//This is the actual damage of the zombie. We assign the visual appearance
|
|
//and range here because of stuff we'll find out later
|
|
var melee = EnsureComp<MeleeWeaponComponent>(target);
|
|
melee.Animation = zombiecomp.AttackAnimation;
|
|
melee.WideAnimation = zombiecomp.AttackAnimation;
|
|
melee.AltDisarm = false;
|
|
melee.Range = 1.2f;
|
|
melee.Angle = 0.0f;
|
|
melee.HitSound = zombiecomp.BiteSound;
|
|
|
|
DirtyFields(target, melee, null, fields:
|
|
[
|
|
nameof(MeleeWeaponComponent.Animation),
|
|
nameof(MeleeWeaponComponent.WideAnimation),
|
|
nameof(MeleeWeaponComponent.AltDisarm),
|
|
nameof(MeleeWeaponComponent.Range),
|
|
nameof(MeleeWeaponComponent.Angle),
|
|
nameof(MeleeWeaponComponent.HitSound),
|
|
]);
|
|
|
|
if (mobState.CurrentState == MobState.Alive)
|
|
{
|
|
// Groaning when damaged
|
|
EnsureComp<EmoteOnDamageComponent>(target);
|
|
_emoteOnDamage.AddEmote(target, "Scream");
|
|
|
|
// Random groaning
|
|
EnsureComp<AutoEmoteComponent>(target);
|
|
_autoEmote.AddEmote(target, "ZombieGroan");
|
|
}
|
|
|
|
if (TryComp<BloodstreamComponent>(target, out var stream) && stream.BloodReferenceSolution is { } reagents)
|
|
{
|
|
zombiecomp.BeforeZombifiedBloodReagents = reagents.Clone();
|
|
// Store the blood refresh amount for cloning later.
|
|
zombiecomp.BeforeZombifiedBloodRefresh = stream.BloodRefreshAmount;
|
|
}
|
|
|
|
if (_visualBody.TryGatherMarkingsData(target, null, out var profiles, out _, out var markings))
|
|
{
|
|
// TODO: My kingdom for ZombieSystem just using cloning system
|
|
zombiecomp.BeforeZombifiedProfiles = profiles;
|
|
zombiecomp.BeforeZombifiedMarkings = markings.ToDictionary(
|
|
kvp => kvp.Key,
|
|
kvp => kvp.Value.ToDictionary(
|
|
it => it.Key,
|
|
it => it.Value.ShallowClone()));
|
|
|
|
var zombifiedProfiles = profiles.ToDictionary(pair => pair.Key,
|
|
pair => pair.Value with { EyeColor = zombiecomp.EyeColor, SkinColor = zombiecomp.SkinColor });
|
|
_visualBody.ApplyProfiles(target, zombifiedProfiles);
|
|
|
|
var newMarkings = markings.ToDictionary(
|
|
kvp => kvp.Key,
|
|
kvp => kvp.Value.ToDictionary(
|
|
it => it.Key,
|
|
it => it.Value.ShallowClone()));
|
|
|
|
foreach (var markingSet in newMarkings.Values)
|
|
{
|
|
foreach (var (layer, layerMarkings) in markingSet)
|
|
{
|
|
if (!AdditionalZombieLayers.Contains(layer))
|
|
continue;
|
|
|
|
for (var i = 0; i < layerMarkings.Count; i++)
|
|
{
|
|
layerMarkings[i] = layerMarkings[i].WithColor(zombiecomp.SkinColor);
|
|
}
|
|
}
|
|
}
|
|
|
|
_visualBody.ApplyMarkings(target, newMarkings);
|
|
}
|
|
|
|
//We have specific stuff for humanoid zombies because they matter more
|
|
if (HasComp<HumanoidProfileComponent>(target))
|
|
{
|
|
//This is done here because non-humanoids shouldn't get baller damage
|
|
melee.Damage = zombiecomp.DamageOnBite;
|
|
|
|
// humanoid zombies get to pry open doors and shit
|
|
var pryComp = EnsureComp<PryingComponent>(target);
|
|
pryComp.SpeedModifier = 0.75f;
|
|
pryComp.PryPowered = true;
|
|
pryComp.Force = true;
|
|
|
|
Dirty(target, pryComp);
|
|
}
|
|
|
|
Dirty(target, melee);
|
|
|
|
//The zombie gets the assigned damage weaknesses and strengths
|
|
_damageable.SetDamageModifierSetId(target, "Zombie");
|
|
|
|
//This makes it so the zombie doesn't take bloodloss damage.
|
|
//NOTE: they are supposed to bleed, just not take damage
|
|
_bloodstream.SetBloodLossThreshold(target, 0f);
|
|
//Give them zombie blood
|
|
_bloodstream.ChangeBloodReagents(target, zombiecomp.NewBloodReagents);
|
|
//Stop their blood from automatically regenerating
|
|
_bloodstream.ChangeBloodRefreshAmount(target, 0f);
|
|
_bloodstream.ChangeBloodIncreaseEnabled(target, false);
|
|
|
|
//This is specifically here to combat insuls, because frying zombies on grilles is funny as shit.
|
|
_inventory.TryUnequip(target, "gloves", true, true);
|
|
//Should prevent instances of zombies using comms for information they shouldnt be able to have.
|
|
_inventory.TryUnequip(target, "ears", true, true);
|
|
|
|
//popup
|
|
_popup.PopupEntity(
|
|
Loc.GetString("zombie-transform", ("target", Identity.Entity(target, EntityManager))),
|
|
target,
|
|
PopupType.LargeCaution);
|
|
|
|
//Make it sentient if it's an animal or something
|
|
_mind.MakeSentient(target);
|
|
|
|
//Make the zombie not die in the cold. Good for space zombies
|
|
if (TryComp<TemperatureDamageComponent>(target, out var tempComp))
|
|
tempComp.ColdDamage.ClampMax(0);
|
|
|
|
//Heals the zombie from all the damage it took while human
|
|
_damageable.ClearAllDamage(target);
|
|
_mobState.ChangeMobState(target, MobState.Alive);
|
|
|
|
_faction.ClearFactions(target, dirty: false);
|
|
_faction.AddFaction(target, ZombieFaction);
|
|
|
|
//gives it the funny "Zombie ___" name.
|
|
_nameMod.RefreshNameModifiers(target);
|
|
|
|
_identity.QueueIdentityUpdate(target);
|
|
|
|
var htn = EnsureComp<HTNComponent>(target);
|
|
htn.RootTask = new HTNCompoundTask() { Task = "SimpleHostileCompound" };
|
|
htn.Blackboard.SetValue(NPCBlackboard.Owner, target);
|
|
_npc.SleepNPC(target, htn);
|
|
|
|
//He's gotta have a mind
|
|
var hasMind = _mind.TryGetMind(target, out var mindId, out var mind);
|
|
if (hasMind && mind != null && _player.TryGetSessionById(mind.UserId, out var session))
|
|
{
|
|
//Zombie role for player manifest
|
|
_role.MindAddRole(mindId, MindRoleZombie, mind: null, silent: true);
|
|
|
|
//Greeting message for new bebe zombers
|
|
_chatMan.DispatchServerMessage(session, Loc.GetString("zombie-infection-greeting"));
|
|
|
|
// Notificate player about new role assignment
|
|
_audio.PlayGlobal(zombiecomp.GreetSoundNotification, session);
|
|
}
|
|
else
|
|
{
|
|
_npc.WakeNPC(target, htn);
|
|
}
|
|
|
|
if (!HasComp<GhostRoleMobSpawnerComponent>(target) && !hasMind) //this specific component gives build test trouble so pop off, ig
|
|
{
|
|
MakeGhostRole(target);
|
|
}
|
|
|
|
// Uncuffing the zombie
|
|
foreach (var cuff in _cuffable.GetAllCuffs(target))
|
|
{
|
|
_cuffable.Uncuff(target, null, cuff);
|
|
}
|
|
|
|
// forcibly empties hands (even if they contain something sticky/unremovable)
|
|
_hands.DropAll(target); // TODO refactor to force drop all if #45844 gets merged
|
|
// temp backup to get rid of unremovable items
|
|
foreach (var hand in _hands.EnumerateHands(target))
|
|
{
|
|
if (_containerSystem.TryGetContainer(target, hand, out var handContainer))
|
|
_containerSystem.EmptyContainer(handContainer, true);
|
|
}
|
|
|
|
// the zombie is now clumsy. it will drop anything handed to it.
|
|
_statusEffects.TrySetStatusEffectDuration(target, ClumsyZombieStatus);
|
|
|
|
// Sloth: What the fuck?
|
|
// How long until compregistry lmao.
|
|
RemComp<PullerComponent>(target);
|
|
|
|
// No longer waiting to become a zombie:
|
|
// Requires deferral because this is (probably) the event which called ZombifyEntity in the first place.
|
|
RemCompDeferred<PendingZombieComponent>(target);
|
|
|
|
//zombie gamemode stuff
|
|
var ev = new EntityZombifiedEvent(target);
|
|
RaiseLocalEvent(target, ref ev, true);
|
|
//zombies revert to their default movement speed.
|
|
_movementSpeedModifier.RefreshMovementSpeedModifiers(target);
|
|
|
|
//Need to prevent them from getting an item, they have no hands.
|
|
// Also prevents them from becoming a Survivor. They're undead.
|
|
_tag.AddTag(target, InvalidForGlobalSpawnSpellTag);
|
|
_tag.AddTag(target, CannotSuicideTag);
|
|
}
|
|
}
|