From 0ae4a6792fe179b318a3147a2994308ab1002144 Mon Sep 17 00:00:00 2001 From: DrSmugleaf Date: Fri, 19 Feb 2021 19:31:25 +0100 Subject: [PATCH] Add health overlay and a command to toggle it (#3278) * Add health overlay bar and a command to toggle it * Remove empty line --- .../Commands/ToggleHealthOverlayCommand.cs | 21 +++ .../HealthOverlay/HealthOverlayBar.cs | 46 +++++ .../HealthOverlay/HealthOverlayGui.cs | 163 ++++++++++++++++++ .../HealthOverlay/HealthOverlaySystem.cs | 106 ++++++++++++ .../Mobs/State/IMobStateComponent.cs | 21 +++ .../Mobs/State/SharedMobStateComponent.cs | 102 +++++++++-- .../Interface/Misc/health_bar.rsi/icon.png | Bin 0 -> 2107 bytes .../Interface/Misc/health_bar.rsi/meta.json | 14 ++ 8 files changed, 458 insertions(+), 15 deletions(-) create mode 100644 Content.Client/Commands/ToggleHealthOverlayCommand.cs create mode 100644 Content.Client/GameObjects/EntitySystems/HealthOverlay/HealthOverlayBar.cs create mode 100644 Content.Client/GameObjects/EntitySystems/HealthOverlay/HealthOverlayGui.cs create mode 100644 Content.Client/GameObjects/EntitySystems/HealthOverlay/HealthOverlaySystem.cs create mode 100644 Resources/Textures/Interface/Misc/health_bar.rsi/icon.png create mode 100644 Resources/Textures/Interface/Misc/health_bar.rsi/meta.json diff --git a/Content.Client/Commands/ToggleHealthOverlayCommand.cs b/Content.Client/Commands/ToggleHealthOverlayCommand.cs new file mode 100644 index 0000000000..361d158c56 --- /dev/null +++ b/Content.Client/Commands/ToggleHealthOverlayCommand.cs @@ -0,0 +1,21 @@ +using Content.Client.GameObjects.EntitySystems.HealthOverlay; +using Robust.Shared.Console; +using Robust.Shared.GameObjects; + +namespace Content.Client.Commands +{ + public class ToggleHealthOverlayCommand : IConsoleCommand + { + public string Command => "togglehealthoverlay"; + public string Description => "Toggles a health bar above mobs."; + public string Help => $"Usage: {Command}"; + + public void Execute(IConsoleShell shell, string argStr, string[] args) + { + var system = EntitySystem.Get(); + system.Enabled = !system.Enabled; + + shell.WriteLine($"Health overlay system {(system.Enabled ? "enabled" : "disabled")}."); + } + } +} diff --git a/Content.Client/GameObjects/EntitySystems/HealthOverlay/HealthOverlayBar.cs b/Content.Client/GameObjects/EntitySystems/HealthOverlay/HealthOverlayBar.cs new file mode 100644 index 0000000000..3e06cf9ca4 --- /dev/null +++ b/Content.Client/GameObjects/EntitySystems/HealthOverlay/HealthOverlayBar.cs @@ -0,0 +1,46 @@ +using Robust.Client.Graphics; +using Robust.Client.UserInterface; +using Robust.Shared.IoC; +using Robust.Shared.Maths; +using Robust.Shared.Prototypes; + +namespace Content.Client.GameObjects.EntitySystems.HealthOverlay +{ + public class HealthOverlayBar : Control + { + public const byte HealthBarScale = 2; + + private const int XPixelDiff = 20 * HealthBarScale; + + public HealthOverlayBar() + { + IoCManager.InjectDependencies(this); + Shader = IoCManager.Resolve().Index("unshaded").Instance(); + } + + private ShaderInstance Shader { get; } + + /// + /// From -1 (dead) to 0 (crit) and 1 (alive) + /// + public float Ratio { get; set; } + + public Color Color { get; set; } + + protected override void Draw(DrawingHandleScreen handle) + { + base.Draw(handle); + + handle.UseShader(Shader); + + var leftOffset = 2 * HealthBarScale; + var box = new UIBox2i( + leftOffset, + -2 + 2 * HealthBarScale, + leftOffset + (int) (XPixelDiff * Ratio * UIScale), + -2); + + handle.DrawRect(box, Color); + } + } +} diff --git a/Content.Client/GameObjects/EntitySystems/HealthOverlay/HealthOverlayGui.cs b/Content.Client/GameObjects/EntitySystems/HealthOverlay/HealthOverlayGui.cs new file mode 100644 index 0000000000..e56cc7078e --- /dev/null +++ b/Content.Client/GameObjects/EntitySystems/HealthOverlay/HealthOverlayGui.cs @@ -0,0 +1,163 @@ +#nullable enable +using Content.Client.Utility; +using Content.Shared.GameObjects.Components.Damage; +using Content.Shared.GameObjects.Components.Mobs.State; +using Robust.Client.Graphics; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Map; +using Robust.Shared.Maths; +using Robust.Shared.Timing; + +namespace Content.Client.GameObjects.EntitySystems.HealthOverlay +{ + public class HealthOverlayGui : VBoxContainer + { + [Dependency] private readonly IEyeManager _eyeManager = default!; + + public HealthOverlayGui(IEntity entity) + { + IoCManager.InjectDependencies(this); + IoCManager.Resolve().StateRoot.AddChild(this); + SeparationOverride = 0; + + CritBar = new HealthOverlayBar + { + Visible = false, + SizeFlagsVertical = SizeFlags.ShrinkCenter, + Color = Color.Red + }; + + HealthBar = new HealthOverlayBar + { + Visible = false, + SizeFlagsVertical = SizeFlags.ShrinkCenter, + Color = Color.Green + }; + + AddChild(Panel = new PanelContainer + { + Children = + { + new TextureRect + { + Texture = StaticIoC.ResC.GetTexture("/Textures/Interface/Misc/health_bar.rsi/icon.png"), + TextureScale = Vector2.One * HealthOverlayBar.HealthBarScale, + SizeFlagsVertical = SizeFlags.ShrinkCenter + }, + CritBar, + HealthBar + } + }); + + Entity = entity; + } + + public PanelContainer Panel { get; } + + public HealthOverlayBar HealthBar { get; } + + public HealthOverlayBar CritBar { get; } + + public IEntity Entity { get; } + + public void SetVisibility(bool val) + { + Visible = val; + Panel.Visible = val; + } + + protected override void Update(FrameEventArgs args) + { + base.Update(args); + + if (Entity.Deleted) + { + return; + } + + if (!Entity.TryGetComponent(out IMobStateComponent? mobState) || + !Entity.TryGetComponent(out IDamageableComponent? damageable)) + { + CritBar.Visible = false; + HealthBar.Visible = false; + return; + } + + int threshold; + + if (mobState.IsAlive()) + { + if (!mobState.TryGetEarliestCriticalState(damageable.TotalDamage, out _, out threshold)) + { + CritBar.Visible = false; + HealthBar.Visible = false; + return; + } + + CritBar.Ratio = 1; + CritBar.Visible = true; + HealthBar.Ratio = 1 - (float) damageable.TotalDamage / threshold; + HealthBar.Visible = true; + } + else if (mobState.IsCritical()) + { + HealthBar.Ratio = 0; + HealthBar.Visible = false; + + if (!mobState.TryGetPreviousCriticalState(damageable.TotalDamage, out _, out var critThreshold) || + !mobState.TryGetEarliestDeadState(damageable.TotalDamage, out _, out var deadThreshold)) + { + CritBar.Visible = false; + return; + } + + CritBar.Visible = true; + CritBar.Ratio = 1 - (float) + (damageable.TotalDamage - critThreshold) / + (deadThreshold - critThreshold); + } + else if (mobState.IsDead()) + { + CritBar.Ratio = 0; + CritBar.Visible = false; + HealthBar.Ratio = 0; + HealthBar.Visible = true; + } + else + { + CritBar.Visible = false; + HealthBar.Visible = false; + } + } + + protected override void FrameUpdate(FrameEventArgs args) + { + base.FrameUpdate(args); + + if (Entity.Deleted || + _eyeManager.CurrentMap != Entity.Transform.MapID) + { + Visible = false; + return; + } + + Visible = true; + + var screenCoordinates = _eyeManager.CoordinatesToScreen(Entity.Transform.Coordinates); + var playerPosition = new ScreenCoordinates(screenCoordinates.X / UIScale, screenCoordinates.Y / UIScale); + LayoutContainer.SetPosition(this, new Vector2(playerPosition.X - Width / 2, playerPosition.Y - Height - 30.0f)); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (!disposing) return; + + HealthBar.Dispose(); + } + } +} diff --git a/Content.Client/GameObjects/EntitySystems/HealthOverlay/HealthOverlaySystem.cs b/Content.Client/GameObjects/EntitySystems/HealthOverlay/HealthOverlaySystem.cs new file mode 100644 index 0000000000..46001650b0 --- /dev/null +++ b/Content.Client/GameObjects/EntitySystems/HealthOverlay/HealthOverlaySystem.cs @@ -0,0 +1,106 @@ +#nullable enable +using System.Collections.Generic; +using Content.Shared.GameObjects.Components.Damage; +using Content.Shared.GameObjects.Components.Mobs.State; +using Content.Shared.GameTicking; +using JetBrains.Annotations; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; + +namespace Content.Client.GameObjects.EntitySystems.HealthOverlay +{ + [UsedImplicitly] + public class HealthOverlaySystem : EntitySystem, IResettingEntitySystem + { + [Dependency] private readonly IEyeManager _eyeManager = default!; + + private readonly Dictionary _guis = new(); + private IEntity? _attachedEntity; + private bool _enabled; + + public bool Enabled + { + get => _enabled; + set + { + if (_enabled == value) + { + return; + } + + _enabled = value; + + foreach (var gui in _guis.Values) + { + gui.SetVisibility(value); + } + } + } + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(HandlePlayerAttached); + } + + public void Reset() + { + foreach (var gui in _guis.Values) + { + gui.Dispose(); + } + + _guis.Clear(); + _attachedEntity = null; + } + + private void HandlePlayerAttached(PlayerAttachSysMessage message) + { + _attachedEntity = message.AttachedEntity; + } + + public override void FrameUpdate(float frameTime) + { + base.Update(frameTime); + + if (!_enabled) + { + return; + } + + if (_attachedEntity == null || _attachedEntity.Deleted) + { + return; + } + + var viewBox = _eyeManager.GetWorldViewport().Enlarged(2.0f); + + foreach (var (mobState, _) in ComponentManager.EntityQuery()) + { + var entity = mobState.Owner; + + if (_attachedEntity.Transform.MapID != entity.Transform.MapID || + !viewBox.Contains(entity.Transform.WorldPosition)) + { + if (_guis.TryGetValue(entity.Uid, out var oldGui)) + { + oldGui.Dispose(); + } + + continue; + } + + if (_guis.ContainsKey(entity.Uid)) + { + continue; + } + + var gui = new HealthOverlayGui(entity); + _guis.Add(entity.Uid, gui); + } + } + } +} diff --git a/Content.Shared/GameObjects/Components/Mobs/State/IMobStateComponent.cs b/Content.Shared/GameObjects/Components/Mobs/State/IMobStateComponent.cs index 7e9bf10e00..1077b6d39e 100644 --- a/Content.Shared/GameObjects/Components/Mobs/State/IMobStateComponent.cs +++ b/Content.Shared/GameObjects/Components/Mobs/State/IMobStateComponent.cs @@ -18,11 +18,32 @@ namespace Content.Shared.GameObjects.Components.Mobs.State (IMobState state, int threshold)? GetEarliestIncapacitatedState(int minimumDamage); + (IMobState state, int threshold)? GetEarliestCriticalState(int minimumDamage); + + (IMobState state, int threshold)? GetEarliestDeadState(int minimumDamage); + + (IMobState state, int threshold)? GetPreviousCriticalState(int maximumDamage); + bool TryGetEarliestIncapacitatedState( int minimumDamage, [NotNullWhen(true)] out IMobState? state, out int threshold); + bool TryGetEarliestCriticalState( + int minimumDamage, + [NotNullWhen(true)] out IMobState? state, + out int threshold); + + bool TryGetEarliestDeadState( + int minimumDamage, + [NotNullWhen(true)] out IMobState? state, + out int threshold); + + bool TryGetPreviousCriticalState( + int maximumDamage, + [NotNullWhen(true)] out IMobState? state, + out int threshold); + void UpdateState(int damage, bool syncing = false); } } diff --git a/Content.Shared/GameObjects/Components/Mobs/State/SharedMobStateComponent.cs b/Content.Shared/GameObjects/Components/Mobs/State/SharedMobStateComponent.cs index e80169cdad..7eea9f528f 100644 --- a/Content.Shared/GameObjects/Components/Mobs/State/SharedMobStateComponent.cs +++ b/Content.Shared/GameObjects/Components/Mobs/State/SharedMobStateComponent.cs @@ -43,6 +43,8 @@ namespace Content.Shared.GameObjects.Components.Mobs.State [ViewVariables] public int? CurrentThreshold { get; private set; } + public IEnumerable> _highestToLowestStates => _lowestToHighestStates.Reverse(); + public override void ExposeData(ObjectSerializer serializer) { base.ExposeData(serializer); @@ -175,16 +177,12 @@ namespace Content.Shared.GameObjects.Components.Mobs.State return true; } - public (IMobState state, int threshold)? GetEarliestIncapacitatedState(int minimumDamage) + private (IMobState state, int threshold)? GetEarliestState(int minimumDamage, Predicate predicate) { foreach (var (threshold, state) in _lowestToHighestStates) { - if (!state.IsIncapacitated()) - { - continue; - } - - if (threshold < minimumDamage) + if (threshold < minimumDamage || + !predicate(state)) { continue; } @@ -195,6 +193,68 @@ namespace Content.Shared.GameObjects.Components.Mobs.State return null; } + private (IMobState state, int threshold)? GetPreviousState(int maximumDamage, Predicate predicate) + { + foreach (var (threshold, state) in _highestToLowestStates) + { + if (threshold > maximumDamage || + !predicate(state)) + { + continue; + } + + return (state, threshold); + } + + return null; + } + + public (IMobState state, int threshold)? GetEarliestCriticalState(int minimumDamage) + { + return GetEarliestState(minimumDamage, s => s.IsCritical()); + } + + public (IMobState state, int threshold)? GetEarliestIncapacitatedState(int minimumDamage) + { + return GetEarliestState(minimumDamage, s => s.IsIncapacitated()); + } + + public (IMobState state, int threshold)? GetEarliestDeadState(int minimumDamage) + { + return GetEarliestState(minimumDamage, s => s.IsDead()); + } + + public (IMobState state, int threshold)? GetPreviousCriticalState(int minimumDamage) + { + return GetPreviousState(minimumDamage, s => s.IsCritical()); + } + + private bool TryGetState( + (IMobState state, int threshold)? tuple, + [NotNullWhen(true)] out IMobState? state, + out int threshold) + { + if (tuple == null) + { + state = default; + threshold = default; + return false; + } + + (state, threshold) = tuple.Value; + return true; + } + + public bool TryGetEarliestCriticalState( + int minimumDamage, + [NotNullWhen(true)] out IMobState? state, + out int threshold) + { + var earliestState = GetEarliestCriticalState(minimumDamage); + + return TryGetState(earliestState, out state, out threshold); + } + public bool TryGetEarliestIncapacitatedState( int minimumDamage, [NotNullWhen(true)] out IMobState? state, @@ -202,15 +262,27 @@ namespace Content.Shared.GameObjects.Components.Mobs.State { var earliestState = GetEarliestIncapacitatedState(minimumDamage); - if (earliestState == null) - { - state = default; - threshold = default; - return false; - } + return TryGetState(earliestState, out state, out threshold); + } - (state, threshold) = earliestState.Value; - return true; + public bool TryGetEarliestDeadState( + int minimumDamage, + [NotNullWhen(true)] out IMobState? state, + out int threshold) + { + var earliestState = GetEarliestDeadState(minimumDamage); + + return TryGetState(earliestState, out state, out threshold); + } + + public bool TryGetPreviousCriticalState( + int maximumDamage, + [NotNullWhen(true)] out IMobState? state, + out int threshold) + { + var earliestState = GetPreviousCriticalState(maximumDamage); + + return TryGetState(earliestState, out state, out threshold); } private void RemoveState(bool syncing = false) diff --git a/Resources/Textures/Interface/Misc/health_bar.rsi/icon.png b/Resources/Textures/Interface/Misc/health_bar.rsi/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..6038bb659d1df7a2942ac882607d9d7ed1cc2e42 GIT binary patch literal 2107 zcmV-B2*me^P) zaB^>EX>4U6ba`-PAZ2)IW&i+q+O1bxcEhR;{bv+s2!N0T!x68w&J6DG&jvebnx;v5 z+c=4BSx7=#FT3%-f2aEgpGYJwv1TbE9-mZFiH3{pkI!)qKJEYG=JG6jraQ0ag9sAg zOvkN^lixt+j|G}edi_jy+DaMHdGJ^7}s?n&|i$etG}PV#Hzi=|TDGKX-NA0|zF1>_308j6U3P2)b-b z?)-Gp1K-ik z<68GoaAa@30kw{;R_7~jN{I0I3@P+rVTQ5KEi{;z;)sfKj4jfpWztw;gVO`0b%Yt! zQK1fWA~hgKAjhjMVe2hRACzTi?g5-JKv>u~-W=|p@c$m41kJg^JcQ-UWZPG;E3Wv0 z84J1f4=w-_=IK-3{tfOp`TNfTmVh8{H)m|nd_6=|4j-{a8)x$Dz?%G#FG2e$00u!f z!K!A;6Cr_!umYlwt(}fgQ&VD`P@Q0XR7>-B_bx^G1#b8RZ2)FbR?* z10VxfDRC0yTSf?(W8%cj!pfNon~)@75m8xn%9J!ItEg($qLwMA%vrMLoJ+P63!#j$ zD^j%NQi>UrS~AEo@EZs#i}s1PBbL+>%tdYt%BN zWItiKR(L`bL0wT{y~ERJU8iW{*@w>7pc$FviLg23&uBB(S=&-l^7{8sRcS=%gHEB@ zVsSnDHZ0bp_s{Y}5$J8A92l~CnL($PIVnsxLVC}fE$OQ%8LhkY#7?%Rdr6X4x^(P)O8=n2(l7zV=5LsM+Vs4^N_PirHlJNmOgP@Rxu$;WQ& zEJ3L;a(Ivo6bM#rRgfYHd(}L9TsiC$53Vbb>3F)DMnVtNA|1ZXZpL1*vs4oX!dKi+ znB9-KpD?>$8ggZJFSuJT(oio8cMKxpYgo-k!0G$;pkxH1Np@`L64nw*ylA!z*aRce zs3VWU;uHLeT!Sqb4AC>n$S&I3Jov93YZN9s3;zC=FxmD?;~v7)zL$ttuLBDjrRr zGa>c{V$aY|<8n6G$#{*@f_YI#fh9Gd`=WLrkWiI+X=sI*G-`uw7yl6{9t&b{43_6pea?CK?AkNb&!qi$?a$p1seVaQwk*$zZJF?|&P(}7A8jnQ`rfA9d;=TX8`0e>y zL1=uRrj9Z$xA5X3;6(-#cX9s#UQf$h8`VAZ0004nX+uL$Nkc;*aB^>EX>4Tx0C=2z zkv&MmKpe$iQ?*4Z4t9{@kfAz=1yK=4sbUcyJ6ykH@ zF@r8h{K$3LR`EpS=msDr--A9s!_g>b6MfM#aXS?SnHnr zg`tABoZ&jnAtbPfBvKF|qlOJsU?E1UMv93v?Z-X*5yzh*mrSk=FmlYJ3Kf#$2mgcL z-I~S82{$Pe2fANu`(qdg>;lcYZGRuzcJl=AKLb}<+h1(}GoPf_+gkJp=-&n|uG^Zt z2VCv|gHO6-NRH&EDHMyq`x$*x9vHXy{D4^000SaNLh0L04^f{04^f|c%?sf z00007bV*G`2jmC_3>yX=RdC_}003V}L_t(2&$W^<4uBvGMPCyWHR?h-5eDvmDhI;U zu&NVI=mnr-hxhW^F96PIWx$kTA`KpV>