EntityHealthBarOverlay.cs 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. using System.Numerics;
  2. using Content.Client.StatusIcon;
  3. using Content.Client.UserInterface.Systems;
  4. using Content.Shared.Damage;
  5. using Content.Shared.FixedPoint;
  6. using Content.Shared.Mobs;
  7. using Content.Shared.Mobs.Components;
  8. using Content.Shared.Mobs.Systems;
  9. using Content.Shared.StatusIcon;
  10. using Content.Shared.StatusIcon.Components;
  11. using Robust.Client.GameObjects;
  12. using Robust.Client.Graphics;
  13. using Robust.Shared.Enums;
  14. using Robust.Shared.Prototypes;
  15. using static Robust.Shared.Maths.Color;
  16. namespace Content.Client.Overlays;
  17. /// <summary>
  18. /// Overlay that shows a health bar on mobs.
  19. /// </summary>
  20. public sealed class EntityHealthBarOverlay : Overlay
  21. {
  22. private readonly IEntityManager _entManager;
  23. private readonly IPrototypeManager _prototype;
  24. private readonly SharedTransformSystem _transform;
  25. private readonly MobStateSystem _mobStateSystem;
  26. private readonly MobThresholdSystem _mobThresholdSystem;
  27. private readonly StatusIconSystem _statusIconSystem;
  28. private readonly ProgressColorSystem _progressColor;
  29. public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV;
  30. public HashSet<string> DamageContainers = new();
  31. public ProtoId<HealthIconPrototype>? StatusIcon;
  32. public EntityHealthBarOverlay(IEntityManager entManager, IPrototypeManager prototype)
  33. {
  34. _entManager = entManager;
  35. _prototype = prototype;
  36. _transform = _entManager.System<SharedTransformSystem>();
  37. _mobStateSystem = _entManager.System<MobStateSystem>();
  38. _mobThresholdSystem = _entManager.System<MobThresholdSystem>();
  39. _statusIconSystem = _entManager.System<StatusIconSystem>();
  40. _progressColor = _entManager.System<ProgressColorSystem>();
  41. }
  42. protected override void Draw(in OverlayDrawArgs args)
  43. {
  44. var handle = args.WorldHandle;
  45. var rotation = args.Viewport.Eye?.Rotation ?? Angle.Zero;
  46. var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
  47. const float scale = 1f;
  48. var scaleMatrix = Matrix3Helpers.CreateScale(new Vector2(scale, scale));
  49. var rotationMatrix = Matrix3Helpers.CreateRotation(-rotation);
  50. _prototype.TryIndex(StatusIcon, out var statusIcon);
  51. var query = _entManager.AllEntityQueryEnumerator<MobThresholdsComponent, MobStateComponent, DamageableComponent, SpriteComponent>();
  52. while (query.MoveNext(out var uid,
  53. out var mobThresholdsComponent,
  54. out var mobStateComponent,
  55. out var damageableComponent,
  56. out var spriteComponent))
  57. {
  58. if (statusIcon != null && !_statusIconSystem.IsVisible((uid, _entManager.GetComponent<MetaDataComponent>(uid)), statusIcon))
  59. continue;
  60. // We want the stealth user to still be able to see his health bar himself
  61. if (!xformQuery.TryGetComponent(uid, out var xform) ||
  62. xform.MapID != args.MapId)
  63. continue;
  64. if (damageableComponent.DamageContainerID == null || !DamageContainers.Contains(damageableComponent.DamageContainerID))
  65. continue;
  66. // we use the status icon component bounds if specified otherwise use sprite
  67. var bounds = _entManager.GetComponentOrNull<StatusIconComponent>(uid)?.Bounds ?? spriteComponent.Bounds;
  68. var worldPos = _transform.GetWorldPosition(xform, xformQuery);
  69. if (!bounds.Translated(worldPos).Intersects(args.WorldAABB))
  70. continue;
  71. // we are all progressing towards death every day
  72. if (CalcProgress(uid, mobStateComponent, damageableComponent, mobThresholdsComponent) is not { } deathProgress)
  73. continue;
  74. var worldPosition = _transform.GetWorldPosition(xform);
  75. var worldMatrix = Matrix3Helpers.CreateTranslation(worldPosition);
  76. var scaledWorld = Matrix3x2.Multiply(scaleMatrix, worldMatrix);
  77. var matty = Matrix3x2.Multiply(rotationMatrix, scaledWorld);
  78. handle.SetTransform(matty);
  79. var yOffset = bounds.Height * EyeManager.PixelsPerMeter / 2 - 3f;
  80. var widthOfMob = bounds.Width * EyeManager.PixelsPerMeter;
  81. var position = new Vector2(-widthOfMob / EyeManager.PixelsPerMeter / 2, yOffset / EyeManager.PixelsPerMeter);
  82. var color = GetProgressColor(deathProgress.ratio, deathProgress.inCrit);
  83. // Hardcoded width of the progress bar because it doesn't match the texture.
  84. const float startX = 8f;
  85. var endX = widthOfMob - 8f;
  86. var xProgress = (endX - startX) * deathProgress.ratio + startX;
  87. var boxBackground = new Box2(new Vector2(startX, 0f) / EyeManager.PixelsPerMeter, new Vector2(endX, 3f) / EyeManager.PixelsPerMeter);
  88. boxBackground = boxBackground.Translated(position);
  89. handle.DrawRect(boxBackground, Black.WithAlpha(192));
  90. var boxMain = new Box2(new Vector2(startX, 0f) / EyeManager.PixelsPerMeter, new Vector2(xProgress, 3f) / EyeManager.PixelsPerMeter);
  91. boxMain = boxMain.Translated(position);
  92. handle.DrawRect(boxMain, color);
  93. var pixelDarken = new Box2(new Vector2(startX, 2f) / EyeManager.PixelsPerMeter, new Vector2(xProgress, 3f) / EyeManager.PixelsPerMeter);
  94. pixelDarken = pixelDarken.Translated(position);
  95. handle.DrawRect(pixelDarken, Black.WithAlpha(128));
  96. }
  97. handle.SetTransform(Matrix3x2.Identity);
  98. }
  99. /// <summary>
  100. /// Returns a ratio between 0 and 1, and whether the entity is in crit.
  101. /// </summary>
  102. private (float ratio, bool inCrit)? CalcProgress(EntityUid uid, MobStateComponent component, DamageableComponent dmg, MobThresholdsComponent thresholds)
  103. {
  104. if (_mobStateSystem.IsAlive(uid, component))
  105. {
  106. if (dmg.HealthBarThreshold != null && dmg.TotalDamage < dmg.HealthBarThreshold)
  107. return null;
  108. if (!_mobThresholdSystem.TryGetThresholdForState(uid, MobState.Critical, out var threshold, thresholds) &&
  109. !_mobThresholdSystem.TryGetThresholdForState(uid, MobState.Dead, out threshold, thresholds))
  110. return (1, false);
  111. var ratio = 1 - ((FixedPoint2) (dmg.TotalDamage / threshold)).Float();
  112. return (ratio, false);
  113. }
  114. if (_mobStateSystem.IsCritical(uid, component))
  115. {
  116. if (!_mobThresholdSystem.TryGetThresholdForState(uid, MobState.Critical, out var critThreshold, thresholds) ||
  117. !_mobThresholdSystem.TryGetThresholdForState(uid, MobState.Dead, out var deadThreshold, thresholds))
  118. {
  119. return (1, true);
  120. }
  121. var ratio = 1 - ((dmg.TotalDamage - critThreshold) / (deadThreshold - critThreshold)).Value.Float();
  122. return (ratio, true);
  123. }
  124. return (0, true);
  125. }
  126. public Color GetProgressColor(float progress, bool crit)
  127. {
  128. if (crit)
  129. progress = 0;
  130. return _progressColor.GetProgressColor(progress);
  131. }
  132. }