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.Server.Speech.EntitySystems; using Content.Shared.Anomaly.Components; using Content.Shared.Armor; using Content.Shared.Bed.Sleep; using Content.Shared.Cloning.Events; using Content.Shared.Chat; using Content.Shared.Damage.Systems; using Content.Shared.Humanoid; 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.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.Shared._Offbrand.Wounds; // Offbrand namespace Content.Server.Zombies { public sealed partial class ZombieSystem : SharedZombieSystem { [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IPrototypeManager _protoManager = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly BloodstreamSystem _bloodstream = default!; [Dependency] private readonly DamageableSystem _damageable = default!; [Dependency] private readonly ChatSystem _chat = default!; [Dependency] private readonly ActionsSystem _actions = default!; [Dependency] private readonly AutoEmoteSystem _autoEmote = default!; [Dependency] private readonly EmoteOnDamageSystem _emoteOnDamage = default!; [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly SharedRoleSystem _role = default!; [Dependency] private readonly Content.Shared._Offbrand.Wounds.BrainDamageSystem _brainDamage = default!; // Offbrand public readonly ProtoId 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(OnEmote, before: new[] { typeof(VocalSystem), typeof(BodyEmotesSystem) }); SubscribeLocalEvent(OnMeleeHit); SubscribeLocalEvent(OnMobState); SubscribeLocalEvent(OnZombieCloning); SubscribeLocalEvent(OnSleepAttempt); SubscribeLocalEvent(OnGetCharacterDeadIC); SubscribeLocalEvent(OnGetCharacterUnrevivableIC); SubscribeLocalEvent(OnMindAdded); SubscribeLocalEvent(OnMindRemoved); SubscribeLocalEvent(OnPendingMapInit); SubscribeLocalEvent(OnBeforeRemoveAnomalyOnDeath); SubscribeLocalEvent(OnPendingMapInit); SubscribeLocalEvent(OnDamageChanged); } private void OnBeforeRemoveAnomalyOnDeath(Entity 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(uid) || HasComp(uid)) return; EnsureComp(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(); 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); _brainDamage.TryChangeBrainDamage(uid, multiplier / 2f); // Offbrand } // Heal the zombified var zombQuery = EntityQueryEnumerator(); 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; _protoManager.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(uid); _emoteOnDamage.AddEmote(uid, "Scream"); // Random groaning EnsureComp(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 entity, ref MeleeHitEvent args) { if (!args.IsHit) return; var cannotSpread = HasComp(args.User); foreach (var uid in args.HitEntities) { if (args.User == uid) continue; if (!TryComp(uid, out var mobState)) continue; if (HasComp(uid) || HasComp(uid)) { // Don't infect, don't deal damage, do not heal from bites, don't pass go! args.Handled = true; continue; } else if (!HasComp(entity)) // Offbrand { if (!HasComp(entity) && !HasComp(args.User) && _random.Prob(GetZombieInfectionChance(entity, component))) { EnsureComp(entity); EnsureComp(entity); } } 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(uid) || cannotSpread || _random.Prob(GetZombieInfectionChance(uid, entity.Comp))) continue; EnsureComp(uid); EnsureComp(uid); } else { if (HasComp(uid) || cannotSpread) continue; // If the target is dead and can be infected, infect. ZombifyEntity(uid); args.Handled = true; } } } /// /// This is the function to call if you want to unzombify an entity. /// /// the entity having the ZombieComponent /// the entity you want to unzombify (different from source in case of cloning, for example) /// /// /// this currently only restore the skin/eye color from before zombified /// TODO: completely rethink how zombies are done to allow reversal. /// public bool UnZombify(EntityUid source, EntityUid target, ZombieComponent? zombiecomp) { if (!Resolve(source, ref zombiecomp)) return false; foreach (var (layer, info) in zombiecomp.BeforeZombifiedCustomBaseLayers) { _humanoidAppearance.SetBaseLayerColor(target, layer, info.Color); _humanoidAppearance.SetBaseLayerId(target, layer, info.Id); } if (TryComp(target, out var appcomp)) { appcomp.EyeColor = zombiecomp.BeforeZombifiedEyeColor; } _humanoidAppearance.SetSkinColor(target, zombiecomp.BeforeZombifiedSkinColor, false); _bloodstream.ChangeBloodReagent(target, zombiecomp.BeforeZombifiedBloodReagent); return true; } private void OnZombieCloning(Entity 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 ent, ref MindAddedMessage args) { if (!_role.MindHasRole(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. private void OnMindRemoved(Entity ent, ref MindRemovedMessage args) { _role.MindRemoveRole((args.Mind.Owner, args.Mind.Comp)); } } }