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: