using Content.Shared.NPC.Prototypes; using System.Text.RegularExpressions; 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.GameTicking.Rules.Components; using Content.Server.Pinpointer; 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.Mech.Components; // Sunrise-Edit 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.Stunnable; using Content.Shared.Throwing; using Content.Shared.Weapons.Melee.Events; using Content.Shared.Zombies; using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Timing; 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 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 ThrowingSystem _throwing = default!; [Dependency] private readonly ActionsSystem _action = default!; [Dependency] private readonly SharedStunSystem _stun = default!; [Dependency] private readonly NavMapSystem _navMap = default!; // Sunrise-Zombies [Dependency] private readonly SharedTransformSystem _transform = default!; 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(OnStartup); 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); // Sunnrise-Start SubscribeLocalEvent(OnJump); SubscribeLocalEvent(OnFlair); SubscribeLocalEvent(OnThrowDoHit); // Sunnrise-End } // Sunnrise-Start private void OnThrowDoHit(EntityUid uid, ZombieComponent component, ThrowDoHitEvent args) { if (_mobState.IsDead(uid)) return; if (HasComp(args.Target) || HasComp(args.Target)) return; if (!_mobState.IsAlive(args.Target)) return; _stun.TryAddParalyzeDuration(args.Target, TimeSpan.FromSeconds(component.ParalyzeTime)); _damageable.TryChangeDamage(args.Target, component.Damage, origin: args.Thrown); } 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 OnFlair(EntityUid uid, ZombieComponent component, ZombieFlairActionEvent args) { if (args.Handled) return; var zombieXform = Transform(uid); EntityUid? nearestUid = default!; TransformComponent? nearestXform = default!; float? minDistance = null; var query = AllEntityQuery(); while (query.MoveNext(out var targetUid, out var humanoidAppearanceComponent)) { // Зомби не должны чувствовать тех, у кого иммунитет к ним. if (HasComp(targetUid) || HasComp(targetUid)) continue; var xform = Transform(targetUid); // Почему бы и нет, оптимизация наху var distance = Math.Abs(zombieXform.Coordinates.X - xform.Coordinates.X) + Math.Abs(zombieXform.Coordinates.Y - xform.Coordinates.Y); if (distance > component.MaxFlairDistance) continue; if (minDistance == null || nearestUid == null || minDistance > distance) { nearestUid = targetUid; minDistance = distance; } } if (nearestUid == null || nearestUid == default!) { _popup.PopupEntity($"Ближайших выживших не найдено.", uid, uid, PopupType.LargeCaution); } else { _popup.PopupEntity($"Ближайший выживший находится {RemoveColorTags(_navMap.GetNearestBeaconString(nearestUid.Value))}", uid, uid, PopupType.LargeCaution); } args.Handled = true; } private string RemoveColorTags(string input) { // Регулярное выражение для поиска тэгов [color=...] и [/color] var pattern = @"\[\s*\/?\s*color(?:=[^\]]*)?\]"; // Заменяем найденные тэги на пустую строку var result = Regex.Replace(input, pattern, string.Empty, RegexOptions.IgnoreCase); return result; } private void OnJump(EntityUid uid, ZombieComponent component, ZombieJumpActionEvent args) { if (args.Handled) return; // TODO: Проверка? // if () // { // _popup.PopupEntity(Loc.GetString("ни магу"), // uid, uid, PopupType.LargeCaution); // return; // } args.Handled = true; var xform = Transform(uid); var mapCoords = args.Target.ToMap(EntityManager, _transform); var direction = mapCoords.Position - xform.MapPosition.Position; if (direction.Length() > component.MaxThrow) { direction = direction.Normalized() * component.MaxThrow; } _throwing.TryThrow(uid, direction, 7F, uid, 10F); _chat.TryEmoteWithChat(uid, "ZombieGroan"); } // Sunnrise-End private void OnPendingMapInit(EntityUid uid, IncurableZombieComponent component, MapInitEvent args) { _action.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); } // 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 OnStartup(EntityUid uid, ZombieComponent component, ComponentStartup args) { // Sunnrise-Start _action.AddAction(uid, component.ActionJumpId); _action.AddAction(uid, component.ActionFlairId); // Sunnrise-End } 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; } // Sunrise-Edit-Start if (HasComp(entity)) continue; // Sunrise-Edit-End 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.ChangeBloodReagents(target, zombiecomp.BeforeZombifiedBloodReagents); 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)); } } }