diff --git a/Content.Client/EntityHealthBar/EntityHealthBarOverlay.cs b/Content.Client/EntityHealthBar/EntityHealthBarOverlay.cs
new file mode 100644
index 00000000000..7b47a660804
--- /dev/null
+++ b/Content.Client/EntityHealthBar/EntityHealthBarOverlay.cs
@@ -0,0 +1,181 @@
+using System.Numerics;
+using Content.Shared.Damage;
+using Content.Shared.Mobs;
+using Content.Shared.Mobs.Components;
+using Content.Shared.Mobs.Systems;
+using Content.Shared.FixedPoint;
+using Content.Shared.Interaction;
+using Content.Shared.Physics;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Client.Player;
+using Robust.Shared.Enums;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Client.EntityHealthBar;
+
+///
+/// Yeah a lot of this is duplicated from doafters.
+/// Not much to be done until there's a generic HUD system
+///
+public sealed class EntityHealthBarOverlay : Overlay
+{
+ [Dependency]
+ private readonly IEntityManager _entManager = default!;
+ [Dependency]
+ private readonly IPrototypeManager _protoManager = default!;
+
+ private readonly SharedTransformSystem _transform;
+ private readonly MobStateSystem _mobStateSystem;
+ private readonly MobThresholdSystem _mobThresholdSystem;
+ private readonly Texture _barTexture;
+ private readonly ShaderInstance _shader;
+ private readonly SharedInteractionSystem _interaction;
+
+ [Dependency]
+ private readonly IPlayerManager _playerManager = default!;
+ public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV;
+ public List DamageContainers = new();
+
+ public EntityHealthBarOverlay()
+ {
+ IoCManager.InjectDependencies(this);
+
+ _transform = _entManager.System();
+ _mobStateSystem = _entManager.System();
+ _mobThresholdSystem = _entManager.System();
+ _interaction = _entManager.System();
+
+ var sprite = new SpriteSpecifier.Rsi(new ("/Textures/Interface/Misc/health_bar.rsi"), "icon");
+ _barTexture = _entManager.EntitySysManager.GetEntitySystem().Frame0(sprite);
+
+ _shader = _protoManager.Index("unshaded").Instance();
+ }
+
+ protected override void Draw(in OverlayDrawArgs args)
+ {
+ if (_playerManager.LocalEntity == null)
+ {
+ return;
+ }
+
+ var handle = args.WorldHandle;
+ var rotation = args.Viewport.Eye?.Rotation ?? Angle.Zero;
+ var spriteQuery = _entManager.GetEntityQuery();
+ var xformQuery = _entManager.GetEntityQuery();
+
+ const float scale = 1f;
+ var scaleMatrix = Matrix3.CreateScale(new Vector2(scale, scale));
+ var rotationMatrix = Matrix3.CreateRotation(-rotation);
+ handle.UseShader(_shader);
+
+ var q = _entManager.AllEntityQueryEnumerator();
+ while (q.MoveNext(out var owner, out var thresholds, out var mob, out var dmg))
+ {
+ if (!xformQuery.TryGetComponent(owner, out var xform) ||
+ xform.MapID != args.MapId)
+ {
+ continue;
+ }
+
+ if (dmg.DamageContainerID == null || !DamageContainers.Contains(dmg.DamageContainerID))
+ continue;
+
+ if (!_interaction.InRangeUnobstructed(_playerManager.LocalEntity.Value, owner, range: 30f, collisionMask: CollisionGroup.Opaque))
+ continue;
+
+
+ var worldPosition = _transform.GetWorldPosition(xform);
+ var worldMatrix = Matrix3.CreateTranslation(worldPosition);
+
+ Matrix3.Multiply(scaleMatrix, worldMatrix, out var scaledWorld);
+ Matrix3.Multiply(rotationMatrix, scaledWorld, out var matty);
+
+ handle.SetTransform(matty);
+
+ float yOffset;
+ if (spriteQuery.TryGetComponent(owner, out var sprite))
+ {
+ yOffset = sprite.Bounds.Height + 15f;
+ }
+ else
+ {
+ yOffset = 1f;
+ }
+
+ var position = new Vector2(-_barTexture.Width / 2f / EyeManager.PixelsPerMeter,
+ yOffset / EyeManager.PixelsPerMeter);
+
+ // Draw the underlying bar texture
+ handle.DrawTexture(_barTexture, position);
+ // we are all progressing towards death every day
+ (float ratio, bool inCrit) deathProgress = CalcProgress(owner, mob, dmg, thresholds);
+
+ var color = GetProgressColor(deathProgress.ratio, deathProgress.inCrit);
+
+ // Hardcoded width of the progress bar because it doesn't match the texture.
+ const float startX = 2f;
+ const float endX = 22f;
+
+ var xProgress = (endX - startX) * deathProgress.ratio + startX;
+
+ var box = new Box2(new Vector2(startX, 3f) / EyeManager.PixelsPerMeter, new Vector2(xProgress, 4f) / EyeManager.PixelsPerMeter);
+ box = box.Translated(position);
+ handle.DrawRect(box, color);
+ }
+
+ handle.UseShader(null);
+ handle.SetTransform(Matrix3.Identity);
+ }
+
+ ///
+ /// Returns a ratio between 0 and 1, and whether the entity is in crit.
+ ///
+ private (float, bool) CalcProgress(EntityUid uid, MobStateComponent component, DamageableComponent dmg, MobThresholdsComponent thresholds)
+ {
+ if (_mobStateSystem.IsAlive(uid, component))
+ {
+ if (!_mobThresholdSystem.TryGetThresholdForState(uid, MobState.Critical, out var threshold, thresholds))
+ return (1, false);
+
+ var ratio = 1 - ((FixedPoint2)(dmg.TotalDamage / threshold)).Float();
+ return (ratio, false);
+ }
+
+ if (_mobStateSystem.IsCritical(uid, component))
+ {
+ if (!_mobThresholdSystem.TryGetThresholdForState(uid, MobState.Critical, out var critThreshold, thresholds) ||
+ !_mobThresholdSystem.TryGetThresholdForState(uid, MobState.Dead, out var deadThreshold, thresholds))
+ {
+ return (1, true);
+ }
+
+ var ratio = 1 -
+ ((dmg.TotalDamage - critThreshold) /
+ (deadThreshold - critThreshold)).Value.Float();
+
+ return (ratio, true);
+ }
+
+ return (0, true);
+ }
+
+ public static Color GetProgressColor(float progress, bool crit)
+ {
+ if (progress >= 1.0f)
+ {
+ return new Color(0f, 1f, 0f);
+ }
+ // lerp
+ if (!crit)
+ {
+ var hue = (5f / 18f) * progress;
+ return Color.FromHsv((hue, 1f, 0.75f, 1f));
+ }
+ else
+ {
+ return Color.Red;
+ }
+ }
+}
diff --git a/Content.Client/EntityHealthBar/ShowHealthBarsSystem.cs b/Content.Client/EntityHealthBar/ShowHealthBarsSystem.cs
new file mode 100644
index 00000000000..1ca36d3dc1a
--- /dev/null
+++ b/Content.Client/EntityHealthBar/ShowHealthBarsSystem.cs
@@ -0,0 +1,85 @@
+using Content.Shared.EntityHealthBar;
+using Content.Shared.GameTicking;
+using Robust.Client.Graphics;
+using Robust.Client.Player;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.EntityHealthBar;
+
+public sealed class ShowHealthBarsSystem : EntitySystem
+{
+ [Dependency] private readonly IPlayerManager _player = default!;
+ [Dependency] private readonly IOverlayManager _overlayMan = default!;
+
+ private EntityHealthBarOverlay _overlay = default!;
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnInit);
+ SubscribeLocalEvent(OnRemove);
+ SubscribeLocalEvent(OnUpdate);
+ SubscribeLocalEvent(OnRoundRestart);
+
+ _player.LocalPlayerAttached += OnPlayerAttached;
+ _player.LocalPlayerDetached += OnPlayerDetached;
+
+ _overlay = new();
+ }
+
+ public override void Shutdown()
+ {
+ base.Shutdown();
+
+ _player.LocalPlayerAttached -= OnPlayerAttached;
+ _player.LocalPlayerDetached -= OnPlayerDetached;
+ }
+
+ private void OnUpdate(Entity ent, ref AfterAutoHandleStateEvent args)
+ {
+ _overlay.DamageContainers.Clear();
+ _overlay.DamageContainers.AddRange(ent.Comp.DamageContainers);
+ }
+
+ private void OnInit(EntityUid uid, ShowHealthBarsComponent component, ComponentInit args)
+ {
+ if (_player.LocalSession?.AttachedEntity == uid)
+ {
+ ApplyOverlays(component);
+ }
+ }
+
+ private void OnRemove(EntityUid uid, ShowHealthBarsComponent component, ComponentRemove args)
+ {
+ if (_player.LocalSession?.AttachedEntity == uid)
+ {
+ _overlayMan.RemoveOverlay(_overlay);
+ }
+ }
+
+ private void OnPlayerAttached(EntityUid uid)
+ {
+ if (TryComp(uid, out var comp))
+ {
+ ApplyOverlays(comp);
+ }
+ }
+
+ private void ApplyOverlays(ShowHealthBarsComponent component)
+ {
+ _overlayMan.AddOverlay(_overlay);
+ _overlay.DamageContainers.Clear();
+ _overlay.DamageContainers.AddRange(component.DamageContainers);
+ }
+
+ private void OnPlayerDetached(EntityUid uid)
+ {
+ _overlayMan.RemoveOverlay(_overlay);
+ }
+
+ private void OnRoundRestart(RoundRestartCleanupEvent args)
+ {
+ _overlayMan.RemoveOverlay(_overlay);
+ }
+}
diff --git a/Content.Client/HealthOverlay/UI/HealthOverlayGui.cs b/Content.Client/HealthOverlay/UI/HealthOverlayGui.cs
index e8ec77e5400..05f80a41160 100644
--- a/Content.Client/HealthOverlay/UI/HealthOverlayGui.cs
+++ b/Content.Client/HealthOverlay/UI/HealthOverlayGui.cs
@@ -89,7 +89,8 @@ private void MoreFrameUpdate()
var mobThresholdSystem = _entities.EntitySysManager.GetEntitySystem();
if (mobStateSystem.IsAlive(Entity, mobState))
{
- if (!mobThresholdSystem.TryGetThresholdForState(Entity,MobState.Critical, out var threshold))
+ if (!(mobThresholdSystem.TryGetThresholdForState(Entity, MobState.Critical, out var threshold) ||
+ mobThresholdSystem.TryGetThresholdForState(Entity, MobState.Dead, out threshold)))
{
CritBar.Visible = false;
HealthBar.Visible = false;
diff --git a/Content.Shared/EntityHealthBar/ShowHealthBarsComponent.cs b/Content.Shared/EntityHealthBar/ShowHealthBarsComponent.cs
new file mode 100644
index 00000000000..c07c002109b
--- /dev/null
+++ b/Content.Shared/EntityHealthBar/ShowHealthBarsComponent.cs
@@ -0,0 +1,23 @@
+using Content.Shared.Damage.Prototypes;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
+
+namespace Content.Shared.EntityHealthBar;
+
+///
+/// This component allows you to see health bars above damageable mobs.
+///
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
+public sealed partial class ShowHealthBarsComponent : Component
+{
+ ///
+ /// If null, displays all health bars.
+ /// If not null, displays health bars of only that damage container.
+ ///
+
+ [AutoNetworkedField]
+ [DataField("damageContainers", customTypeSerializer: typeof(PrototypeIdListSerializer))]
+ public List DamageContainers = new();
+
+ public override bool SendOnlyToOwner => true;
+}
diff --git a/Resources/Prototypes/Entities/Clothing/Eyes/hud.yml b/Resources/Prototypes/Entities/Clothing/Eyes/hud.yml
index 051df2348fc..db3e52fea61 100644
--- a/Resources/Prototypes/Entities/Clothing/Eyes/hud.yml
+++ b/Resources/Prototypes/Entities/Clothing/Eyes/hud.yml
@@ -8,11 +8,12 @@
sprite: Clothing/Eyes/Hud/diag.rsi
- type: Clothing
sprite: Clothing/Eyes/Hud/diag.rsi
-# - type: ClothingGrantComponent
-# component:
-# - type: ShowHealthBars
-# damageContainers:
-# - Inorganic
+ - type: ClothingGrantComponent
+ component:
+ - type: ShowHealthBars
+ damageContainers:
+ - Inorganic
+ - Silicon
- type: ReverseEngineering # Nyano
difficulty: 3
recipes:
@@ -28,12 +29,11 @@
sprite: Clothing/Eyes/Hud/med.rsi
- type: Clothing
sprite: Clothing/Eyes/Hud/med.rsi
-# - type: ClothingGrantComponent
-# component:
-# - type: ShowHealthBars
-# damageContainers:
-# - Biological
-# - HalfSpirit
+ - type: ClothingGrantComponent
+ component:
+ - type: ShowHealthBars
+ damageContainers:
+ - Biological
- type: ReverseEngineering # Nyano
difficulty: 3
recipes: