1
0

ZombieSystem.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. using System.Linq;
  2. using Content.Server.Actions;
  3. using Content.Server.Body.Systems;
  4. using Content.Server.Chat;
  5. using Content.Server.Chat.Systems;
  6. using Content.Server.Emoting.Systems;
  7. using Content.Server.Speech.EntitySystems;
  8. using Content.Server.Roles;
  9. using Content.Shared.Anomaly.Components;
  10. using Content.Shared.Bed.Sleep;
  11. using Content.Shared.Cloning.Events;
  12. using Content.Shared.Damage;
  13. using Content.Shared.Humanoid;
  14. using Content.Shared.Inventory;
  15. using Content.Shared.Mind;
  16. using Content.Shared.Mind.Components;
  17. using Content.Shared.Mobs;
  18. using Content.Shared.Mobs.Components;
  19. using Content.Shared.Mobs.Systems;
  20. using Content.Shared.Popups;
  21. using Content.Shared.Roles;
  22. using Content.Shared.Weapons.Melee.Events;
  23. using Content.Shared.Zombies;
  24. using Robust.Shared.Prototypes;
  25. using Robust.Shared.Random;
  26. using Robust.Shared.Timing;
  27. namespace Content.Server.Zombies
  28. {
  29. public sealed partial class ZombieSystem : SharedZombieSystem
  30. {
  31. [Dependency] private readonly IGameTiming _timing = default!;
  32. [Dependency] private readonly IPrototypeManager _protoManager = default!;
  33. [Dependency] private readonly IRobustRandom _random = default!;
  34. [Dependency] private readonly BloodstreamSystem _bloodstream = default!;
  35. [Dependency] private readonly DamageableSystem _damageable = default!;
  36. [Dependency] private readonly ChatSystem _chat = default!;
  37. [Dependency] private readonly ActionsSystem _actions = default!;
  38. [Dependency] private readonly AutoEmoteSystem _autoEmote = default!;
  39. [Dependency] private readonly EmoteOnDamageSystem _emoteOnDamage = default!;
  40. [Dependency] private readonly MobStateSystem _mobState = default!;
  41. [Dependency] private readonly SharedPopupSystem _popup = default!;
  42. [Dependency] private readonly SharedRoleSystem _role = default!;
  43. public const SlotFlags ProtectiveSlots =
  44. SlotFlags.FEET |
  45. SlotFlags.HEAD |
  46. SlotFlags.EYES |
  47. SlotFlags.GLOVES |
  48. SlotFlags.MASK |
  49. SlotFlags.NECK |
  50. SlotFlags.INNERCLOTHING |
  51. SlotFlags.OUTERCLOTHING;
  52. public override void Initialize()
  53. {
  54. base.Initialize();
  55. SubscribeLocalEvent<ZombieComponent, ComponentStartup>(OnStartup);
  56. SubscribeLocalEvent<ZombieComponent, EmoteEvent>(OnEmote, before:
  57. new[] { typeof(VocalSystem), typeof(BodyEmotesSystem) });
  58. SubscribeLocalEvent<ZombieComponent, MeleeHitEvent>(OnMeleeHit);
  59. SubscribeLocalEvent<ZombieComponent, MobStateChangedEvent>(OnMobState);
  60. SubscribeLocalEvent<ZombieComponent, CloningEvent>(OnZombieCloning);
  61. SubscribeLocalEvent<ZombieComponent, TryingToSleepEvent>(OnSleepAttempt);
  62. SubscribeLocalEvent<ZombieComponent, GetCharactedDeadIcEvent>(OnGetCharacterDeadIC);
  63. SubscribeLocalEvent<ZombieComponent, MindAddedMessage>(OnMindAdded);
  64. SubscribeLocalEvent<ZombieComponent, MindRemovedMessage>(OnMindRemoved);
  65. SubscribeLocalEvent<PendingZombieComponent, MapInitEvent>(OnPendingMapInit);
  66. SubscribeLocalEvent<PendingZombieComponent, BeforeRemoveAnomalyOnDeathEvent>(OnBeforeRemoveAnomalyOnDeath);
  67. SubscribeLocalEvent<IncurableZombieComponent, MapInitEvent>(OnPendingMapInit);
  68. SubscribeLocalEvent<ZombifyOnDeathComponent, MobStateChangedEvent>(OnDamageChanged);
  69. }
  70. private void OnBeforeRemoveAnomalyOnDeath(Entity<PendingZombieComponent> ent, ref BeforeRemoveAnomalyOnDeathEvent args)
  71. {
  72. // Pending zombies (e.g. infected non-zombies) do not remove their hosted anomaly on death.
  73. // Current zombies DO remove the anomaly on death.
  74. args.Cancelled = true;
  75. }
  76. private void OnPendingMapInit(EntityUid uid, IncurableZombieComponent component, MapInitEvent args)
  77. {
  78. _actions.AddAction(uid, ref component.Action, component.ZombifySelfActionPrototype);
  79. }
  80. private void OnPendingMapInit(EntityUid uid, PendingZombieComponent component, MapInitEvent args)
  81. {
  82. if (_mobState.IsDead(uid))
  83. {
  84. ZombifyEntity(uid);
  85. return;
  86. }
  87. component.NextTick = _timing.CurTime + TimeSpan.FromSeconds(1f);
  88. component.GracePeriod = _random.Next(component.MinInitialInfectedGrace, component.MaxInitialInfectedGrace);
  89. }
  90. public override void Update(float frameTime)
  91. {
  92. base.Update(frameTime);
  93. var curTime = _timing.CurTime;
  94. // Hurt the living infected
  95. var query = EntityQueryEnumerator<PendingZombieComponent, DamageableComponent, MobStateComponent>();
  96. while (query.MoveNext(out var uid, out var comp, out var damage, out var mobState))
  97. {
  98. // Process only once per second
  99. if (comp.NextTick > curTime)
  100. continue;
  101. comp.NextTick = curTime + TimeSpan.FromSeconds(1f);
  102. comp.GracePeriod -= TimeSpan.FromSeconds(1f);
  103. if (comp.GracePeriod > TimeSpan.Zero)
  104. continue;
  105. if (_random.Prob(comp.InfectionWarningChance))
  106. _popup.PopupEntity(Loc.GetString(_random.Pick(comp.InfectionWarnings)), uid, uid);
  107. var multiplier = _mobState.IsCritical(uid, mobState)
  108. ? comp.CritDamageMultiplier
  109. : 1f;
  110. _damageable.TryChangeDamage(uid, comp.Damage * multiplier, true, false, damage);
  111. }
  112. // Heal the zombified
  113. var zombQuery = EntityQueryEnumerator<ZombieComponent, DamageableComponent, MobStateComponent>();
  114. while (zombQuery.MoveNext(out var uid, out var comp, out var damage, out var mobState))
  115. {
  116. // Process only once per second
  117. if (comp.NextTick + TimeSpan.FromSeconds(1) > curTime)
  118. continue;
  119. comp.NextTick = curTime;
  120. if (_mobState.IsDead(uid, mobState))
  121. continue;
  122. var multiplier = _mobState.IsCritical(uid, mobState)
  123. ? comp.PassiveHealingCritMultiplier
  124. : 1f;
  125. // Gradual healing for living zombies.
  126. _damageable.TryChangeDamage(uid, comp.PassiveHealing * multiplier, true, false, damage);
  127. }
  128. }
  129. private void OnSleepAttempt(EntityUid uid, ZombieComponent component, ref TryingToSleepEvent args)
  130. {
  131. args.Cancelled = true;
  132. }
  133. private void OnGetCharacterDeadIC(EntityUid uid, ZombieComponent component, ref GetCharactedDeadIcEvent args)
  134. {
  135. args.Dead = true;
  136. }
  137. private void OnStartup(EntityUid uid, ZombieComponent component, ComponentStartup args)
  138. {
  139. if (component.EmoteSoundsId == null)
  140. return;
  141. _protoManager.TryIndex(component.EmoteSoundsId, out component.EmoteSounds);
  142. }
  143. private void OnEmote(EntityUid uid, ZombieComponent component, ref EmoteEvent args)
  144. {
  145. // always play zombie emote sounds and ignore others
  146. if (args.Handled)
  147. return;
  148. args.Handled = _chat.TryPlayEmoteSound(uid, component.EmoteSounds, args.Emote);
  149. }
  150. private void OnMobState(EntityUid uid, ZombieComponent component, MobStateChangedEvent args)
  151. {
  152. if (args.NewMobState == MobState.Alive)
  153. {
  154. // Groaning when damaged
  155. EnsureComp<EmoteOnDamageComponent>(uid);
  156. _emoteOnDamage.AddEmote(uid, "Scream");
  157. // Random groaning
  158. EnsureComp<AutoEmoteComponent>(uid);
  159. _autoEmote.AddEmote(uid, "ZombieGroan");
  160. }
  161. else
  162. {
  163. // Stop groaning when damaged
  164. _emoteOnDamage.RemoveEmote(uid, "Scream");
  165. // Stop random groaning
  166. _autoEmote.RemoveEmote(uid, "ZombieGroan");
  167. }
  168. }
  169. private float GetZombieInfectionChance(EntityUid uid, ZombieComponent component)
  170. {
  171. var max = component.MaxZombieInfectionChance;
  172. if (!_inventory.TryGetContainerSlotEnumerator(uid, out var enumerator, ProtectiveSlots))
  173. return max;
  174. var items = 0f;
  175. var total = 0f;
  176. while (enumerator.MoveNext(out var con))
  177. {
  178. total++;
  179. if (con.ContainedEntity != null)
  180. items++;
  181. }
  182. if (total == 0)
  183. return max;
  184. // Everyone knows that when it comes to zombies, socks & sandals provide just as much protection as an
  185. // armored vest. Maybe these should be weighted per-item. I.e. some kind of coverage/protection component.
  186. // Or at the very least different weights per slot.
  187. var min = component.MinZombieInfectionChance;
  188. //gets a value between the max and min based on how many items the entity is wearing
  189. var chance = (max - min) * ((total - items) / total) + min;
  190. return chance;
  191. }
  192. private void OnMeleeHit(EntityUid uid, ZombieComponent component, MeleeHitEvent args)
  193. {
  194. if (!TryComp<ZombieComponent>(args.User, out _))
  195. return;
  196. if (!args.HitEntities.Any())
  197. return;
  198. foreach (var entity in args.HitEntities)
  199. {
  200. if (args.User == entity)
  201. continue;
  202. if (!TryComp<MobStateComponent>(entity, out var mobState))
  203. continue;
  204. if (HasComp<ZombieComponent>(entity))
  205. {
  206. args.BonusDamage = -args.BaseDamage;
  207. }
  208. else
  209. {
  210. if (!HasComp<ZombieImmuneComponent>(entity) && !HasComp<NonSpreaderZombieComponent>(args.User) && _random.Prob(GetZombieInfectionChance(entity, component)))
  211. {
  212. EnsureComp<PendingZombieComponent>(entity);
  213. EnsureComp<ZombifyOnDeathComponent>(entity);
  214. }
  215. }
  216. if (_mobState.IsIncapacitated(entity, mobState) && !HasComp<ZombieComponent>(entity) && !HasComp<ZombieImmuneComponent>(entity))
  217. {
  218. ZombifyEntity(entity);
  219. args.BonusDamage = -args.BaseDamage;
  220. }
  221. else if (mobState.CurrentState == MobState.Alive) //heals when zombies bite live entities
  222. {
  223. _damageable.TryChangeDamage(uid, component.HealingOnBite, true, false);
  224. }
  225. }
  226. }
  227. /// <summary>
  228. /// This is the function to call if you want to unzombify an entity.
  229. /// </summary>
  230. /// <param name="source">the entity having the ZombieComponent</param>
  231. /// <param name="target">the entity you want to unzombify (different from source in case of cloning, for example)</param>
  232. /// <param name="zombiecomp"></param>
  233. /// <remarks>
  234. /// this currently only restore the skin/eye color from before zombified
  235. /// TODO: completely rethink how zombies are done to allow reversal.
  236. /// </remarks>
  237. public bool UnZombify(EntityUid source, EntityUid target, ZombieComponent? zombiecomp)
  238. {
  239. if (!Resolve(source, ref zombiecomp))
  240. return false;
  241. foreach (var (layer, info) in zombiecomp.BeforeZombifiedCustomBaseLayers)
  242. {
  243. _humanoidAppearance.SetBaseLayerColor(target, layer, info.Color);
  244. _humanoidAppearance.SetBaseLayerId(target, layer, info.Id);
  245. }
  246. if (TryComp<HumanoidAppearanceComponent>(target, out var appcomp))
  247. {
  248. appcomp.EyeColor = zombiecomp.BeforeZombifiedEyeColor;
  249. }
  250. _humanoidAppearance.SetSkinColor(target, zombiecomp.BeforeZombifiedSkinColor, false);
  251. _bloodstream.ChangeBloodReagent(target, zombiecomp.BeforeZombifiedBloodReagent);
  252. return true;
  253. }
  254. private void OnZombieCloning(Entity<ZombieComponent> ent, ref CloningEvent args)
  255. {
  256. UnZombify(ent.Owner, args.CloneUid, ent.Comp);
  257. }
  258. // Make sure players that enter a zombie (for example via a ghost role or the mind swap spell) count as an antagonist.
  259. private void OnMindAdded(Entity<ZombieComponent> ent, ref MindAddedMessage args)
  260. {
  261. if (!_role.MindHasRole<ZombieRoleComponent>(args.Mind))
  262. _role.MindAddRole(args.Mind, "MindRoleZombie", mind: args.Mind.Comp);
  263. }
  264. // Remove the role when getting cloned, getting gibbed and borged, or leaving the body via any other method.
  265. private void OnMindRemoved(Entity<ZombieComponent> ent, ref MindRemovedMessage args)
  266. {
  267. _role.MindTryRemoveRole<ZombieRoleComponent>(args.Mind);
  268. }
  269. }
  270. }