1
0

MobThresholdSystem.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. using System.Diagnostics.CodeAnalysis;
  2. using System.Linq;
  3. using Content.Shared.Alert;
  4. using Content.Shared.Damage;
  5. using Content.Shared.FixedPoint;
  6. using Content.Shared.Mobs.Components;
  7. using Content.Shared.Mobs.Events;
  8. using Robust.Shared.GameStates;
  9. namespace Content.Shared.Mobs.Systems;
  10. public sealed class MobThresholdSystem : EntitySystem
  11. {
  12. [Dependency] private readonly MobStateSystem _mobStateSystem = default!;
  13. [Dependency] private readonly AlertsSystem _alerts = default!;
  14. public override void Initialize()
  15. {
  16. SubscribeLocalEvent<MobThresholdsComponent, ComponentGetState>(OnGetState);
  17. SubscribeLocalEvent<MobThresholdsComponent, ComponentHandleState>(OnHandleState);
  18. SubscribeLocalEvent<MobThresholdsComponent, ComponentShutdown>(MobThresholdShutdown);
  19. SubscribeLocalEvent<MobThresholdsComponent, ComponentStartup>(MobThresholdStartup);
  20. SubscribeLocalEvent<MobThresholdsComponent, DamageChangedEvent>(OnDamaged);
  21. SubscribeLocalEvent<MobThresholdsComponent, UpdateMobStateEvent>(OnUpdateMobState);
  22. SubscribeLocalEvent<MobThresholdsComponent, MobStateChangedEvent>(OnThresholdsMobState);
  23. }
  24. private void OnGetState(EntityUid uid, MobThresholdsComponent component, ref ComponentGetState args)
  25. {
  26. var thresholds = new Dictionary<FixedPoint2, MobState>();
  27. foreach (var (key, value) in component.Thresholds)
  28. {
  29. thresholds.Add(key, value);
  30. }
  31. args.State = new MobThresholdsComponentState(thresholds,
  32. component.TriggersAlerts,
  33. component.CurrentThresholdState,
  34. component.StateAlertDict,
  35. component.ShowOverlays,
  36. component.AllowRevives);
  37. }
  38. private void OnHandleState(EntityUid uid, MobThresholdsComponent component, ref ComponentHandleState args)
  39. {
  40. if (args.Current is not MobThresholdsComponentState state)
  41. return;
  42. component.Thresholds = new SortedDictionary<FixedPoint2, MobState>(state.UnsortedThresholds);
  43. component.TriggersAlerts = state.TriggersAlerts;
  44. component.CurrentThresholdState = state.CurrentThresholdState;
  45. component.AllowRevives = state.AllowRevives;
  46. }
  47. #region Public API
  48. /// <summary>
  49. /// Gets the next available state for a mob.
  50. /// </summary>
  51. /// <param name="target">Target entity</param>
  52. /// <param name="mobState">Supplied MobState</param>
  53. /// <param name="nextState">The following MobState. Can be null if there isn't one.</param>
  54. /// <param name="thresholdsComponent">Threshold Component Owned by the target</param>
  55. /// <returns>True if the next mob state exists</returns>
  56. public bool TryGetNextState(
  57. EntityUid target,
  58. MobState mobState,
  59. [NotNullWhen(true)] out MobState? nextState,
  60. MobThresholdsComponent? thresholdsComponent = null)
  61. {
  62. nextState = null;
  63. if (!Resolve(target, ref thresholdsComponent))
  64. return false;
  65. MobState? min = null;
  66. foreach (var state in thresholdsComponent.Thresholds.Values)
  67. {
  68. if (state <= mobState)
  69. continue;
  70. if (min == null || state < min)
  71. min = state;
  72. }
  73. nextState = min;
  74. return nextState != null;
  75. }
  76. /// <summary>
  77. /// Get the Damage Threshold for the appropriate state if it exists
  78. /// </summary>
  79. /// <param name="target">Target Entity</param>
  80. /// <param name="mobState">MobState we want the Damage Threshold of</param>
  81. /// <param name="thresholdComponent">Threshold Component Owned by the target</param>
  82. /// <returns>the threshold or 0 if it doesn't exist</returns>
  83. public FixedPoint2 GetThresholdForState(EntityUid target, MobState mobState,
  84. MobThresholdsComponent? thresholdComponent = null)
  85. {
  86. if (!Resolve(target, ref thresholdComponent))
  87. return FixedPoint2.Zero;
  88. foreach (var pair in thresholdComponent.Thresholds)
  89. {
  90. if (pair.Value == mobState)
  91. {
  92. return pair.Key;
  93. }
  94. }
  95. return FixedPoint2.Zero;
  96. }
  97. /// <summary>
  98. /// Try to get the Damage Threshold for the appropriate state if it exists
  99. /// </summary>
  100. /// <param name="target">Target Entity</param>
  101. /// <param name="mobState">MobState we want the Damage Threshold of</param>
  102. /// <param name="threshold">The damage Threshold for the given state</param>
  103. /// <param name="thresholdComponent">Threshold Component Owned by the target</param>
  104. /// <returns>true if successfully retrieved a threshold</returns>
  105. public bool TryGetThresholdForState(EntityUid target, MobState mobState,
  106. [NotNullWhen(true)] out FixedPoint2? threshold,
  107. MobThresholdsComponent? thresholdComponent = null)
  108. {
  109. threshold = null;
  110. if (!Resolve(target, ref thresholdComponent))
  111. return false;
  112. foreach (var pair in thresholdComponent.Thresholds)
  113. {
  114. if (pair.Value == mobState)
  115. {
  116. threshold = pair.Key;
  117. return true;
  118. }
  119. }
  120. return false;
  121. }
  122. /// <summary>
  123. /// Try to get the a percentage of the Damage Threshold for the appropriate state if it exists
  124. /// </summary>
  125. /// <param name="target">Target Entity</param>
  126. /// <param name="mobState">MobState we want the Damage Threshold of</param>
  127. /// <param name="damage">The Damage being applied</param>
  128. /// <param name="percentage">Percentage of Damage compared to the Threshold</param>
  129. /// <param name="thresholdComponent">Threshold Component Owned by the target</param>
  130. /// <returns>true if successfully retrieved a percentage</returns>
  131. public bool TryGetPercentageForState(EntityUid target, MobState mobState, FixedPoint2 damage,
  132. [NotNullWhen(true)] out FixedPoint2? percentage,
  133. MobThresholdsComponent? thresholdComponent = null)
  134. {
  135. percentage = null;
  136. if (!TryGetThresholdForState(target, mobState, out var threshold, thresholdComponent))
  137. return false;
  138. percentage = damage / threshold;
  139. return true;
  140. }
  141. /// <summary>
  142. /// Try to get the Damage Threshold for crit or death. Outputs the first found threshold.
  143. /// </summary>
  144. /// <param name="target">Target Entity</param>
  145. /// <param name="threshold">The Damage Threshold for incapacitation</param>
  146. /// <param name="thresholdComponent">Threshold Component owned by the target</param>
  147. /// <returns>true if successfully retrieved incapacitation threshold</returns>
  148. public bool TryGetIncapThreshold(EntityUid target, [NotNullWhen(true)] out FixedPoint2? threshold,
  149. MobThresholdsComponent? thresholdComponent = null)
  150. {
  151. threshold = null;
  152. if (!Resolve(target, ref thresholdComponent))
  153. return false;
  154. return TryGetThresholdForState(target, MobState.Critical, out threshold, thresholdComponent)
  155. || TryGetThresholdForState(target, MobState.Dead, out threshold, thresholdComponent);
  156. }
  157. /// <summary>
  158. /// Try to get a percentage of the Damage Threshold for crit or death. Outputs the first found percentage.
  159. /// </summary>
  160. /// <param name="target">Target Entity</param>
  161. /// <param name="damage">The damage being applied</param>
  162. /// <param name="percentage">Percentage of Damage compared to the Incapacitation Threshold</param>
  163. /// <param name="thresholdComponent">Threshold Component Owned by the target</param>
  164. /// <returns>true if successfully retrieved incapacitation percentage</returns>
  165. public bool TryGetIncapPercentage(EntityUid target, FixedPoint2 damage,
  166. [NotNullWhen(true)] out FixedPoint2? percentage,
  167. MobThresholdsComponent? thresholdComponent = null)
  168. {
  169. percentage = null;
  170. if (!TryGetIncapThreshold(target, out var threshold, thresholdComponent))
  171. return false;
  172. if (damage == 0)
  173. {
  174. percentage = 0;
  175. return true;
  176. }
  177. percentage = FixedPoint2.Min(1.0f, damage / threshold.Value);
  178. return true;
  179. }
  180. /// <summary>
  181. /// Try to get the Damage Threshold for death
  182. /// </summary>
  183. /// <param name="target">Target Entity</param>
  184. /// <param name="threshold">The Damage Threshold for death</param>
  185. /// <param name="thresholdComponent">Threshold Component owned by the target</param>
  186. /// <returns>true if successfully retrieved incapacitation threshold</returns>
  187. public bool TryGetDeadThreshold(EntityUid target, [NotNullWhen(true)] out FixedPoint2? threshold,
  188. MobThresholdsComponent? thresholdComponent = null)
  189. {
  190. threshold = null;
  191. if (!Resolve(target, ref thresholdComponent, false))
  192. return false;
  193. return TryGetThresholdForState(target, MobState.Dead, out threshold, thresholdComponent);
  194. }
  195. /// <summary>
  196. /// Try to get a percentage of the Damage Threshold for death
  197. /// </summary>
  198. /// <param name="target">Target Entity</param>
  199. /// <param name="damage">The damage being applied</param>
  200. /// <param name="percentage">Percentage of Damage compared to the Death Threshold</param>
  201. /// <param name="thresholdComponent">Threshold Component Owned by the target</param>
  202. /// <returns>true if successfully retrieved death percentage</returns>
  203. public bool TryGetDeadPercentage(EntityUid target, FixedPoint2 damage,
  204. [NotNullWhen(true)] out FixedPoint2? percentage,
  205. MobThresholdsComponent? thresholdComponent = null)
  206. {
  207. percentage = null;
  208. if (!TryGetDeadThreshold(target, out var threshold, thresholdComponent))
  209. return false;
  210. if (damage == 0)
  211. {
  212. percentage = 0;
  213. return true;
  214. }
  215. percentage = FixedPoint2.Min(1.0f, damage / threshold.Value);
  216. return true;
  217. }
  218. /// <summary>
  219. /// Takes the damage from one entity and scales it relative to the health of another
  220. /// </summary>
  221. /// <param name="target1">The entity whose damage will be scaled</param>
  222. /// <param name="target2">The entity whose health the damage will scale to</param>
  223. /// <param name="damage">The newly scaled damage. Can be null</param>
  224. public bool GetScaledDamage(EntityUid target1, EntityUid target2, out DamageSpecifier? damage)
  225. {
  226. damage = null;
  227. if (!TryComp<DamageableComponent>(target1, out var oldDamage))
  228. return false;
  229. if (!TryComp<MobThresholdsComponent>(target1, out var threshold1) ||
  230. !TryComp<MobThresholdsComponent>(target2, out var threshold2))
  231. return false;
  232. if (!TryGetThresholdForState(target1, MobState.Dead, out var ent1DeadThreshold, threshold1))
  233. ent1DeadThreshold = 0;
  234. if (!TryGetThresholdForState(target2, MobState.Dead, out var ent2DeadThreshold, threshold2))
  235. ent2DeadThreshold = 0;
  236. damage = (oldDamage.Damage / ent1DeadThreshold.Value) * ent2DeadThreshold.Value;
  237. return true;
  238. }
  239. /// <summary>
  240. /// Set a MobState Threshold or create a new one if it doesn't exist
  241. /// </summary>
  242. /// <param name="target">Target Entity</param>
  243. /// <param name="damage">Damageable Component owned by the target</param>
  244. /// <param name="mobState">MobState Component owned by the target</param>
  245. /// <param name="threshold">MobThreshold Component owned by the target</param>
  246. public void SetMobStateThreshold(EntityUid target, FixedPoint2 damage, MobState mobState,
  247. MobThresholdsComponent? threshold = null)
  248. {
  249. if (!Resolve(target, ref threshold))
  250. return;
  251. // create a duplicate dictionary so we don't modify while enumerating.
  252. var thresholds = new Dictionary<FixedPoint2, MobState>(threshold.Thresholds);
  253. foreach (var (damageThreshold, state) in thresholds)
  254. {
  255. if (state != mobState)
  256. continue;
  257. threshold.Thresholds.Remove(damageThreshold);
  258. }
  259. threshold.Thresholds[damage] = mobState;
  260. Dirty(target, threshold);
  261. VerifyThresholds(target, threshold);
  262. }
  263. /// <summary>
  264. /// Checks to see if we should change states based on thresholds.
  265. /// Call this if you change the amount of damagable without triggering a damageChangedEvent or if you change
  266. /// </summary>
  267. /// <param name="target">Target Entity</param>
  268. /// <param name="threshold">Threshold Component owned by the Target</param>
  269. /// <param name="mobState">MobState Component owned by the Target</param>
  270. /// <param name="damageable">Damageable Component owned by the Target</param>
  271. public void VerifyThresholds(EntityUid target, MobThresholdsComponent? threshold = null,
  272. MobStateComponent? mobState = null, DamageableComponent? damageable = null)
  273. {
  274. if (!Resolve(target, ref mobState, ref threshold, ref damageable))
  275. return;
  276. CheckThresholds(target, mobState, threshold, damageable);
  277. var ev = new MobThresholdChecked(target, mobState, threshold, damageable);
  278. RaiseLocalEvent(target, ref ev, true);
  279. UpdateAlerts(target, mobState.CurrentState, threshold, damageable);
  280. }
  281. public void SetAllowRevives(EntityUid uid, bool val, MobThresholdsComponent? component = null)
  282. {
  283. if (!Resolve(uid, ref component, false))
  284. return;
  285. component.AllowRevives = val;
  286. Dirty(uid, component);
  287. VerifyThresholds(uid, component);
  288. }
  289. #endregion
  290. #region Private Implementation
  291. private void CheckThresholds(EntityUid target, MobStateComponent mobStateComponent,
  292. MobThresholdsComponent thresholdsComponent, DamageableComponent damageableComponent, EntityUid? origin = null)
  293. {
  294. foreach (var (threshold, mobState) in thresholdsComponent.Thresholds.Reverse())
  295. {
  296. if (damageableComponent.TotalDamage < threshold)
  297. continue;
  298. TriggerThreshold(target, mobState, mobStateComponent, thresholdsComponent, origin);
  299. break;
  300. }
  301. }
  302. private void TriggerThreshold(
  303. EntityUid target,
  304. MobState newState,
  305. MobStateComponent? mobState = null,
  306. MobThresholdsComponent? thresholds = null,
  307. EntityUid? origin = null)
  308. {
  309. if (!Resolve(target, ref mobState, ref thresholds) ||
  310. mobState.CurrentState == newState)
  311. {
  312. return;
  313. }
  314. if (mobState.CurrentState != MobState.Dead || thresholds.AllowRevives)
  315. {
  316. thresholds.CurrentThresholdState = newState;
  317. Dirty(target, thresholds);
  318. }
  319. _mobStateSystem.UpdateMobState(target, mobState, origin);
  320. }
  321. private void UpdateAlerts(EntityUid target, MobState currentMobState, MobThresholdsComponent? threshold = null,
  322. DamageableComponent? damageable = null)
  323. {
  324. if (!Resolve(target, ref threshold, ref damageable))
  325. return;
  326. // don't handle alerts if they are managed by another system... BobbySim (soon TM)
  327. if (!threshold.TriggersAlerts)
  328. return;
  329. if (!threshold.StateAlertDict.TryGetValue(currentMobState, out var currentAlert))
  330. {
  331. Log.Error($"No alert alert for mob state {currentMobState} for entity {ToPrettyString(target)}");
  332. return;
  333. }
  334. if (!_alerts.TryGet(currentAlert, out var alertPrototype))
  335. {
  336. Log.Error($"Invalid alert type {currentAlert}");
  337. return;
  338. }
  339. if (alertPrototype.SupportsSeverity)
  340. {
  341. var severity = _alerts.GetMinSeverity(currentAlert);
  342. var ev = new BeforeAlertSeverityCheckEvent(currentAlert, severity);
  343. RaiseLocalEvent(target, ev);
  344. if (ev.CancelUpdate)
  345. {
  346. _alerts.ShowAlert(target, ev.CurrentAlert, ev.Severity);
  347. return;
  348. }
  349. if (TryGetNextState(target, currentMobState, out var nextState, threshold) &&
  350. TryGetPercentageForState(target, nextState.Value, damageable.TotalDamage, out var percentage))
  351. {
  352. percentage = FixedPoint2.Clamp(percentage.Value, 0, 1);
  353. severity = (short) MathF.Round(
  354. MathHelper.Lerp(
  355. _alerts.GetMinSeverity(currentAlert),
  356. _alerts.GetMaxSeverity(currentAlert),
  357. percentage.Value.Float()));
  358. }
  359. _alerts.ShowAlert(target, currentAlert, severity);
  360. }
  361. else
  362. {
  363. _alerts.ShowAlert(target, currentAlert);
  364. }
  365. }
  366. private void OnDamaged(EntityUid target, MobThresholdsComponent thresholds, DamageChangedEvent args)
  367. {
  368. if (!TryComp<MobStateComponent>(target, out var mobState))
  369. return;
  370. CheckThresholds(target, mobState, thresholds, args.Damageable, args.Origin);
  371. var ev = new MobThresholdChecked(target, mobState, thresholds, args.Damageable);
  372. RaiseLocalEvent(target, ref ev, true);
  373. UpdateAlerts(target, mobState.CurrentState, thresholds, args.Damageable);
  374. }
  375. private void MobThresholdStartup(EntityUid target, MobThresholdsComponent thresholds, ComponentStartup args)
  376. {
  377. if (!TryComp<MobStateComponent>(target, out var mobState) || !TryComp<DamageableComponent>(target, out var damageable))
  378. return;
  379. CheckThresholds(target, mobState, thresholds, damageable);
  380. UpdateAllEffects((target, thresholds, mobState, damageable), mobState.CurrentState);
  381. }
  382. private void MobThresholdShutdown(EntityUid target, MobThresholdsComponent component, ComponentShutdown args)
  383. {
  384. if (component.TriggersAlerts)
  385. _alerts.ClearAlertCategory(target, component.HealthAlertCategory);
  386. }
  387. private void OnUpdateMobState(EntityUid target, MobThresholdsComponent component, ref UpdateMobStateEvent args)
  388. {
  389. if (!component.AllowRevives && component.CurrentThresholdState == MobState.Dead)
  390. {
  391. args.State = MobState.Dead;
  392. }
  393. else if (component.CurrentThresholdState != MobState.Invalid)
  394. {
  395. args.State = component.CurrentThresholdState;
  396. }
  397. }
  398. private void UpdateAllEffects(Entity<MobThresholdsComponent, MobStateComponent?, DamageableComponent?> ent, MobState currentState)
  399. {
  400. var (_, thresholds, mobState, damageable) = ent;
  401. if (Resolve(ent, ref thresholds, ref mobState, ref damageable))
  402. {
  403. var ev = new MobThresholdChecked(ent, mobState, thresholds, damageable);
  404. RaiseLocalEvent(ent, ref ev, true);
  405. }
  406. UpdateAlerts(ent, currentState, thresholds, damageable);
  407. }
  408. private void OnThresholdsMobState(Entity<MobThresholdsComponent> ent, ref MobStateChangedEvent args)
  409. {
  410. UpdateAllEffects((ent, ent, null, null), args.NewMobState);
  411. }
  412. #endregion
  413. }
  414. /// <summary>
  415. /// Event that triggers when an entity with a mob threshold is checked
  416. /// </summary>
  417. /// <param name="Target">Target entity</param>
  418. /// <param name="Threshold">Threshold Component owned by the Target</param>
  419. /// <param name="MobState">MobState Component owned by the Target</param>
  420. /// <param name="Damageable">Damageable Component owned by the Target</param>
  421. [ByRefEvent]
  422. public readonly record struct MobThresholdChecked(EntityUid Target, MobStateComponent MobState,
  423. MobThresholdsComponent Threshold, DamageableComponent Damageable);