SharedBodySystem.Targeting.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  1. // SPDX-FileCopyrightText: 2024 Piras314 <p1r4s@proton.me>
  2. // SPDX-FileCopyrightText: 2024 Skubman <ba.fallaria@gmail.com>
  3. // SPDX-FileCopyrightText: 2024 gluesniffler <159397573+gluesniffler@users.noreply.github.com>
  4. // SPDX-FileCopyrightText: 2024 whateverusername0 <whateveremail>
  5. // SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
  6. // SPDX-FileCopyrightText: 2025 Aviu00 <93730715+Aviu00@users.noreply.github.com>
  7. // SPDX-FileCopyrightText: 2025 Aviu00 <aviu00@protonmail.com>
  8. // SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
  9. // SPDX-FileCopyrightText: 2025 deltanedas <39013340+deltanedas@users.noreply.github.com>
  10. // SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
  11. //
  12. // SPDX-License-Identifier: AGPL-3.0-or-later
  13. using Content.Shared.Body.Components;
  14. using Content.Shared.Body.Part;
  15. using Content.Shared._Shitmed.Body.Events;
  16. using Content.Shared.Damage;
  17. using Content.Shared.Damage.Prototypes;
  18. using Content.Shared.FixedPoint;
  19. using Content.Shared.IdentityManagement;
  20. using Content.Shared._Shitmed.Medical.Surgery.Steps.Parts;
  21. using Content.Shared.Mobs.Components;
  22. using Content.Shared.Mobs.Systems;
  23. using Content.Shared.Popups;
  24. using Content.Shared.Standing;
  25. using Content.Shared._Shitmed.Targeting;
  26. using Content.Shared._Shitmed.Targeting.Events;
  27. using Robust.Shared.CPUJob.JobQueues;
  28. using Robust.Shared.CPUJob.JobQueues.Queues;
  29. using Robust.Shared.Network;
  30. using Robust.Shared.Prototypes;
  31. using Robust.Shared.Random;
  32. using Robust.Shared.Timing;
  33. using System.Linq;
  34. using System.Threading;
  35. using System.Threading.Tasks;
  36. using Content.Shared.Inventory;
  37. // Namespace has set accessors, leaving it on the default.
  38. namespace Content.Shared.Body.Systems;
  39. public partial class SharedBodySystem
  40. {
  41. [Dependency] private readonly INetManager _net = default!;
  42. [Dependency] private readonly MobStateSystem _mobState = default!;
  43. [Dependency] private readonly IRobustRandom _random = default!;
  44. [Dependency] private readonly DamageableSystem _damageable = default!;
  45. [Dependency] private readonly StandingStateSystem _standing = default!;
  46. [Dependency] private readonly SharedPopupSystem _popup = default!;
  47. private readonly ProtoId<DamageTypePrototype>[] _severingDamageTypes = { "Slash", "Piercing", "Blunt" };
  48. private const double IntegrityJobTime = 0.005;
  49. private readonly JobQueue _integrityJobQueue = new(IntegrityJobTime);
  50. public sealed class IntegrityJob : Job<object>
  51. {
  52. private readonly SharedBodySystem _self;
  53. private readonly Entity<BodyPartComponent> _ent;
  54. public IntegrityJob(SharedBodySystem self, Entity<BodyPartComponent> ent, double maxTime, CancellationToken cancellation = default) : base(maxTime, cancellation)
  55. {
  56. _self = self;
  57. _ent = ent;
  58. }
  59. public IntegrityJob(SharedBodySystem self, Entity<BodyPartComponent> ent, double maxTime, IStopwatch stopwatch, CancellationToken cancellation = default) : base(maxTime, stopwatch, cancellation)
  60. {
  61. _self = self;
  62. _ent = ent;
  63. }
  64. protected override Task<object?> Process()
  65. {
  66. _self.ProcessIntegrityTick(_ent);
  67. return Task.FromResult<object?>(null);
  68. }
  69. }
  70. private EntityQuery<TargetingComponent> _queryTargeting;
  71. private void InitializeIntegrityQueue()
  72. {
  73. _queryTargeting = GetEntityQuery<TargetingComponent>();
  74. SubscribeLocalEvent<BodyComponent, TryChangePartDamageEvent>(OnTryChangePartDamage);
  75. SubscribeLocalEvent<BodyComponent, DamageModifyEvent>(OnBodyDamageModify);
  76. SubscribeLocalEvent<BodyPartComponent, DamageModifyEvent>(OnPartDamageModify);
  77. SubscribeLocalEvent<BodyPartComponent, DamageChangedEvent>(OnDamageChanged);
  78. }
  79. private void ProcessIntegrityTick(Entity<BodyPartComponent> entity)
  80. {
  81. if (!TryComp<DamageableComponent>(entity, out var damageable))
  82. return;
  83. var damage = damageable.TotalDamage;
  84. if (entity.Comp is { Body: { } body }
  85. && damage > entity.Comp.MinIntegrity
  86. && damage <= entity.Comp.IntegrityThresholds[TargetIntegrity.HeavilyWounded]
  87. && _queryTargeting.HasComp(body)
  88. && !_mobState.IsDead(body))
  89. _damageable.TryChangeDamage(entity, GetHealingSpecifier(entity), canSever: false, targetPart: GetTargetBodyPart(entity));
  90. }
  91. public override void Update(float frameTime)
  92. {
  93. base.Update(frameTime);
  94. _integrityJobQueue.Process();
  95. if (!_timing.IsFirstTimePredicted)
  96. return;
  97. using var query = EntityQueryEnumerator<BodyPartComponent>();
  98. while (query.MoveNext(out var ent, out var part))
  99. {
  100. part.HealingTimer += frameTime;
  101. if (part.HealingTimer >= part.HealingTime)
  102. {
  103. part.HealingTimer = 0;
  104. _integrityJobQueue.EnqueueJob(new IntegrityJob(this, (ent, part), IntegrityJobTime));
  105. }
  106. }
  107. }
  108. private void OnTryChangePartDamage(Entity<BodyComponent> ent, ref TryChangePartDamageEvent args)
  109. {
  110. // If our target has a TargetingComponent, that means they will take limb damage
  111. // And if their attacker also has one, then we use that part.
  112. if (_queryTargeting.TryComp(ent, out var targetEnt))
  113. {
  114. var damage = args.Damage;
  115. TargetBodyPart? targetPart = null;
  116. if (args.TargetPart != null)
  117. {
  118. targetPart = args.TargetPart;
  119. }
  120. else if (args.Origin.HasValue && _queryTargeting.TryComp(args.Origin.Value, out var targeter))
  121. {
  122. targetPart = targeter.Target;
  123. // If the target is Torso then have a 33% chance to hit another part
  124. if (targetPart.Value == TargetBodyPart.Torso)
  125. {
  126. var additionalPart = GetRandomPartSpread(10);
  127. targetPart = targetPart.Value | additionalPart;
  128. }
  129. }
  130. else
  131. {
  132. // If there's an origin in this case, that means it comes from an entity without TargetingComponent,
  133. // such as an animal, so we attack a random part.
  134. if (args.Origin.HasValue)
  135. {
  136. // Evasion would trigger constantly if we don't target torso
  137. targetPart = args.CanEvade ? TargetBodyPart.Torso : GetRandomBodyPart(ent, targetEnt);
  138. }
  139. // Otherwise we damage all parts equally (barotrauma, explosions, etc).
  140. else if (damage != null)
  141. {
  142. // Division by 2 cuz damaging all parts by the same damage by default is too much.
  143. damage /= 2;
  144. targetPart = TargetBodyPart.All;
  145. }
  146. }
  147. if (targetPart == null)
  148. return;
  149. if (!TryChangePartDamage(ent, args.Damage, args.IgnoreResistances, args.ArmorPenetration, args.CanSever, args.CanEvade, args.PartMultiplier, targetPart.Value, out var evaded)
  150. && args.CanEvade && evaded)
  151. {
  152. if (_net.IsServer)
  153. _popup.PopupEntity(Loc.GetString("surgery-part-damage-evaded", ("user", Identity.Entity(ent, EntityManager))), ent);
  154. args.Evaded = true;
  155. }
  156. }
  157. }
  158. private void OnBodyDamageModify(Entity<BodyComponent> bodyEnt, ref DamageModifyEvent args)
  159. {
  160. if (args.TargetPart != null)
  161. {
  162. var (targetType, _) = ConvertTargetBodyPart(args.TargetPart.Value);
  163. args.Damage *= GetPartDamageModifier(targetType);
  164. }
  165. }
  166. private void OnPartDamageModify(Entity<BodyPartComponent> partEnt, ref DamageModifyEvent args)
  167. {
  168. if (partEnt.Comp.Body != null
  169. && TryComp(partEnt.Comp.Body.Value, out InventoryComponent? inventory))
  170. _inventory.RelayEvent((partEnt.Comp.Body.Value, inventory), ref args);
  171. if (Prototypes.TryIndex<DamageModifierSetPrototype>("PartDamage", out var partModifierSet))
  172. args.Damage = DamageSpecifier.ApplyModifierSet(args.Damage, partModifierSet);
  173. args.Damage *= GetPartDamageModifier(partEnt.Comp.PartType);
  174. }
  175. private bool TryChangePartDamage(EntityUid entity,
  176. DamageSpecifier damage,
  177. bool ignoreResistances,
  178. float armorPenetration,
  179. bool canSever,
  180. bool canEvade,
  181. float partMultiplier,
  182. TargetBodyPart targetParts,
  183. out bool evaded)
  184. {
  185. evaded = false;
  186. if (damage.GetTotal() == 0)
  187. return false;
  188. var landed = false;
  189. var targets = SharedTargetingSystem.GetValidParts();
  190. foreach (var target in targets)
  191. {
  192. if (!targetParts.HasFlag(target))
  193. continue;
  194. var (targetType, targetSymmetry) = ConvertTargetBodyPart(target);
  195. if (GetBodyChildrenOfType(entity, targetType, symmetry: targetSymmetry) is { } part)
  196. {
  197. if (canEvade && TryEvadeDamage(entity, GetEvadeChance(targetType)))
  198. {
  199. evaded = true;
  200. continue;
  201. }
  202. var damageResult = _damageable.TryChangeDamage(part.FirstOrDefault().Id, damage * partMultiplier, ignoreResistances, canSever: canSever, armorPenetration: armorPenetration);
  203. if (damageResult != null && damageResult.GetTotal() != 0)
  204. landed = true;
  205. }
  206. }
  207. return landed;
  208. }
  209. private void OnDamageChanged(Entity<BodyPartComponent> partEnt, ref DamageChangedEvent args)
  210. {
  211. if (!TryComp<DamageableComponent>(partEnt, out var damageable))
  212. return;
  213. var severed = false;
  214. var partIdSlot = GetParentPartAndSlotOrNull(partEnt)?.Slot;
  215. var delta = args.DamageDelta;
  216. if (args.CanSever
  217. && partEnt.Comp.CanSever
  218. && partIdSlot is not null
  219. && delta != null
  220. && !HasComp<BodyPartReattachedComponent>(partEnt)
  221. && !partEnt.Comp.Enabled
  222. && damageable.TotalDamage >= partEnt.Comp.SeverIntegrity
  223. && _severingDamageTypes.Any(damageType => delta.DamageDict.TryGetValue(damageType, out var value) && value > 0))
  224. severed = true;
  225. CheckBodyPart(partEnt, GetTargetBodyPart(partEnt), severed, damageable);
  226. if (severed)
  227. DropPart(partEnt);
  228. Dirty(partEnt, partEnt.Comp);
  229. }
  230. /// <summary>
  231. /// Gets the random body part rolling a number between 1 and 9, and returns
  232. /// Torso if the result is 9 or more. The higher torsoWeight is, the higher chance to return it.
  233. /// By default, the chance to return Torso is 50%.
  234. /// </summary>
  235. private TargetBodyPart GetRandomPartSpread(ushort torsoWeight = 9)
  236. {
  237. var rand = new System.Random((int)_timing.CurTick.Value);
  238. const int targetPartsAmount = 9;
  239. // 5 = amount of target parts except Torso
  240. return rand.Next(1, targetPartsAmount + torsoWeight) switch
  241. {
  242. 1 => TargetBodyPart.Head,
  243. 2 => TargetBodyPart.RightArm,
  244. 3 => TargetBodyPart.RightHand,
  245. 4 => TargetBodyPart.LeftArm,
  246. 5 => TargetBodyPart.LeftHand,
  247. 6 => TargetBodyPart.RightLeg,
  248. 7 => TargetBodyPart.RightFoot,
  249. 8 => TargetBodyPart.LeftLeg,
  250. 9 => TargetBodyPart.LeftFoot,
  251. _ => TargetBodyPart.Torso,
  252. };
  253. }
  254. public TargetBodyPart? GetRandomBodyPart(EntityUid uid, TargetingComponent? target = null)
  255. {
  256. if (!Resolve(uid, ref target, false))
  257. return null;
  258. var rand = new System.Random((int)_timing.CurTick.Value);
  259. var totalWeight = target.TargetOdds.Values.Sum();
  260. var randomValue = rand.NextFloat() * totalWeight;
  261. foreach (var (part, weight) in target.TargetOdds)
  262. {
  263. if (randomValue <= weight)
  264. return part;
  265. randomValue -= weight;
  266. }
  267. return TargetBodyPart.Torso; // Default to torso if something goes wrong
  268. }
  269. /// <summary>
  270. /// This should be called after body part damage was changed.
  271. /// </summary>
  272. public void CheckBodyPart(
  273. Entity<BodyPartComponent> partEnt,
  274. TargetBodyPart? targetPart,
  275. bool severed,
  276. DamageableComponent? damageable = null)
  277. {
  278. if (!Resolve(partEnt, ref damageable))
  279. return;
  280. var integrity = damageable.TotalDamage;
  281. // KILL the body part
  282. if (partEnt.Comp.Enabled && integrity >= partEnt.Comp.IntegrityThresholds[TargetIntegrity.CriticallyWounded])
  283. {
  284. var ev = new BodyPartEnableChangedEvent(false);
  285. RaiseLocalEvent(partEnt, ref ev);
  286. }
  287. // LIVE the body part
  288. if (!partEnt.Comp.Enabled && integrity <= partEnt.Comp.IntegrityThresholds[partEnt.Comp.EnableIntegrity] && !severed)
  289. {
  290. var ev = new BodyPartEnableChangedEvent(true);
  291. RaiseLocalEvent(partEnt, ref ev);
  292. }
  293. if (_queryTargeting.TryComp(partEnt.Comp.Body, out var targeting)
  294. && HasComp<MobStateComponent>(partEnt.Comp.Body))
  295. {
  296. var newIntegrity = GetIntegrityThreshold(partEnt.Comp, integrity.Float(), severed);
  297. // We need to check if the part is dead to prevent the UI from showing dead parts as alive.
  298. if (targetPart is not null &&
  299. targeting.BodyStatus.ContainsKey(targetPart.Value) &&
  300. targeting.BodyStatus[targetPart.Value] != TargetIntegrity.Dead)
  301. {
  302. targeting.BodyStatus[targetPart.Value] = newIntegrity;
  303. if (targetPart.Value == TargetBodyPart.Torso)
  304. targeting.BodyStatus[TargetBodyPart.Groin] = newIntegrity;
  305. Dirty(partEnt.Comp.Body.Value, targeting);
  306. }
  307. // Revival events are handled by the server, so we end up being locked to a network event.
  308. // I hope you like the _net.IsServer, Remuchi :)
  309. if (_net.IsServer)
  310. RaiseNetworkEvent(new TargetIntegrityChangeEvent(GetNetEntity(partEnt.Comp.Body.Value)), partEnt.Comp.Body.Value);
  311. }
  312. }
  313. /// <summary>
  314. /// Gets the integrity of all body parts in the entity.
  315. /// </summary>
  316. public Dictionary<TargetBodyPart, TargetIntegrity> GetBodyPartStatus(EntityUid entityUid)
  317. {
  318. var result = new Dictionary<TargetBodyPart, TargetIntegrity>();
  319. if (!TryComp<BodyComponent>(entityUid, out var body))
  320. return result;
  321. foreach (var part in SharedTargetingSystem.GetValidParts())
  322. {
  323. result[part] = TargetIntegrity.Severed;
  324. }
  325. foreach (var partComponent in GetBodyChildren(entityUid, body))
  326. {
  327. var targetBodyPart = GetTargetBodyPart(partComponent.Component.PartType, partComponent.Component.Symmetry);
  328. if (targetBodyPart != null && TryComp<DamageableComponent>(partComponent.Id, out var damageable))
  329. result[targetBodyPart.Value] = GetIntegrityThreshold(partComponent.Component, damageable.TotalDamage.Float(), false);
  330. }
  331. // Hardcoded shitcode for Groin :)
  332. result[TargetBodyPart.Groin] = result[TargetBodyPart.Torso];
  333. return result;
  334. }
  335. public TargetBodyPart? GetTargetBodyPart(Entity<BodyPartComponent> part) => GetTargetBodyPart(part.Comp.PartType, part.Comp.Symmetry);
  336. public TargetBodyPart? GetTargetBodyPart(BodyPartComponent part) => GetTargetBodyPart(part.PartType, part.Symmetry);
  337. /// <summary>
  338. /// Converts Enums from BodyPartType to their Targeting system equivalent.
  339. /// </summary>
  340. public TargetBodyPart? GetTargetBodyPart(BodyPartType type, BodyPartSymmetry symmetry)
  341. {
  342. return (type, symmetry) switch
  343. {
  344. (BodyPartType.Head, _) => TargetBodyPart.Head,
  345. (BodyPartType.Torso, _) => TargetBodyPart.Torso,
  346. (BodyPartType.Arm, BodyPartSymmetry.Left) => TargetBodyPart.LeftArm,
  347. (BodyPartType.Arm, BodyPartSymmetry.Right) => TargetBodyPart.RightArm,
  348. (BodyPartType.Hand, BodyPartSymmetry.Left) => TargetBodyPart.LeftHand,
  349. (BodyPartType.Hand, BodyPartSymmetry.Right) => TargetBodyPart.RightHand,
  350. (BodyPartType.Leg, BodyPartSymmetry.Left) => TargetBodyPart.LeftLeg,
  351. (BodyPartType.Leg, BodyPartSymmetry.Right) => TargetBodyPart.RightLeg,
  352. (BodyPartType.Foot, BodyPartSymmetry.Left) => TargetBodyPart.LeftFoot,
  353. (BodyPartType.Foot, BodyPartSymmetry.Right) => TargetBodyPart.RightFoot,
  354. _ => null
  355. };
  356. }
  357. /// <summary>
  358. /// Converts Enums from Targeting system to their BodyPartType equivalent.
  359. /// </summary>
  360. public (BodyPartType Type, BodyPartSymmetry Symmetry) ConvertTargetBodyPart(TargetBodyPart targetPart)
  361. {
  362. return targetPart switch
  363. {
  364. TargetBodyPart.Head => (BodyPartType.Head, BodyPartSymmetry.None),
  365. TargetBodyPart.Torso => (BodyPartType.Torso, BodyPartSymmetry.None),
  366. TargetBodyPart.Groin => (BodyPartType.Torso, BodyPartSymmetry.None), // TODO: Groin is not a part type yet
  367. TargetBodyPart.LeftArm => (BodyPartType.Arm, BodyPartSymmetry.Left),
  368. TargetBodyPart.LeftHand => (BodyPartType.Hand, BodyPartSymmetry.Left),
  369. TargetBodyPart.RightArm => (BodyPartType.Arm, BodyPartSymmetry.Right),
  370. TargetBodyPart.RightHand => (BodyPartType.Hand, BodyPartSymmetry.Right),
  371. TargetBodyPart.LeftLeg => (BodyPartType.Leg, BodyPartSymmetry.Left),
  372. TargetBodyPart.LeftFoot => (BodyPartType.Foot, BodyPartSymmetry.Left),
  373. TargetBodyPart.RightLeg => (BodyPartType.Leg, BodyPartSymmetry.Right),
  374. TargetBodyPart.RightFoot => (BodyPartType.Foot, BodyPartSymmetry.Right),
  375. _ => (BodyPartType.Torso, BodyPartSymmetry.None)
  376. };
  377. }
  378. public DamageSpecifier GetHealingSpecifier(BodyPartComponent part)
  379. {
  380. var damage = new DamageSpecifier()
  381. {
  382. DamageDict = new Dictionary<string, FixedPoint2>()
  383. {
  384. { "Blunt", -part.SelfHealingAmount },
  385. { "Slash", -part.SelfHealingAmount },
  386. { "Piercing", -part.SelfHealingAmount },
  387. { "Heat", -part.SelfHealingAmount },
  388. { "Cold", -part.SelfHealingAmount },
  389. { "Shock", -part.SelfHealingAmount },
  390. { "Caustic", -part.SelfHealingAmount * 0.1}, // not much caustic healing
  391. }
  392. };
  393. return damage;
  394. }
  395. /// <summary>
  396. /// Fetches the damage multiplier for part integrity based on part types.
  397. /// </summary>
  398. /// TODO: Serialize this per body part.
  399. public static float GetPartDamageModifier(BodyPartType partType)
  400. {
  401. return partType switch
  402. {
  403. BodyPartType.Head => 0.5f, // 50% damage, necks are hard to cut
  404. BodyPartType.Torso => 1.0f, // 100% damage
  405. BodyPartType.Arm => 0.7f, // 70% damage
  406. BodyPartType.Hand => 0.7f, // 70% damage
  407. BodyPartType.Leg => 0.7f, // 70% damage
  408. BodyPartType.Foot => 0.7f, // 70% damage
  409. _ => 0.5f
  410. };
  411. }
  412. /// <summary>
  413. /// Fetches the TargetIntegrity equivalent of the current integrity value for the body part.
  414. /// </summary>
  415. public static TargetIntegrity GetIntegrityThreshold(BodyPartComponent component, float integrity, bool severed)
  416. {
  417. if (severed)
  418. return TargetIntegrity.Severed;
  419. else if (!component.Enabled)
  420. return TargetIntegrity.Disabled;
  421. var targetIntegrity = TargetIntegrity.Healthy;
  422. foreach (var threshold in component.IntegrityThresholds)
  423. {
  424. if (integrity <= threshold.Value)
  425. targetIntegrity = threshold.Key;
  426. }
  427. return targetIntegrity;
  428. }
  429. /// <summary>
  430. /// Fetches the chance to evade integrity damage for a body part.
  431. /// Used when the entity is not dead, laying down, or incapacitated.
  432. /// </summary>
  433. public static float GetEvadeChance(BodyPartType partType)
  434. {
  435. return partType switch
  436. {
  437. BodyPartType.Head => 0.70f, // 70% chance to evade
  438. BodyPartType.Arm => 0f, // 0% chance to evade
  439. BodyPartType.Hand => 0f, // 0% chance to evade
  440. BodyPartType.Leg => 0f, // 0% chance to evade
  441. BodyPartType.Foot => 0f, // 0% chance to evade
  442. BodyPartType.Torso => 0f, // 0% chance to evade
  443. _ => 0f
  444. };
  445. }
  446. public bool CanEvadeDamage(EntityUid uid)
  447. {
  448. return !_mobState.IsIncapacitated(uid) && !_standing.IsDown(uid);
  449. }
  450. public bool TryEvadeDamage(EntityUid uid, float evadeChance)
  451. {
  452. if (!CanEvadeDamage(uid))
  453. return false;
  454. if (evadeChance == 0f)
  455. return false;
  456. var rand = new System.Random((int)_timing.CurTick.Value);
  457. return rand.Prob(evadeChance);
  458. }
  459. }