Refactor SpeciesUI into overlay and status effects (#381)

* Refactor SpeciesUI into overlay and status effects

All components that update the UI will need to use PlayerAttached for cases where the Mind transfers I think.

* Change overlay / status effects to use states

* Change TryRemoveStatus to RemoveStatus

Doesn't return a bool so not trying.
Addressing PJB's feedback.
This commit is contained in:
metalgearsloth
2019-10-31 02:37:22 +11:00
committed by Pieter-Jan Briers
parent 6497cdf8ff
commit 12cf5559c2
16 changed files with 431 additions and 241 deletions

View File

@@ -110,6 +110,7 @@ namespace Content.Client
"Airlock",
"MedicalScanner",
"WirePlacer",
"Species",
};
foreach (var ignoreName in registerIgnore)

View File

@@ -0,0 +1,107 @@
using System.Collections.Generic;
using Content.Client.Graphics.Overlays;
using Content.Shared.GameObjects.Components.Mobs;
using Robust.Client.GameObjects;
using Robust.Client.Graphics.Overlays;
using Robust.Client.Interfaces.Graphics.Overlays;
using Robust.Client.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.IoC;
using Robust.Shared.Log;
namespace Content.Client.GameObjects
{
/// <summary>
/// A character UI component which shows the current damage state of the mob (living/dead)
/// </summary>
[RegisterComponent]
[ComponentReference(typeof(SharedOverlayEffectsComponent))]
public sealed class ClientOverlayEffectsComponent : SharedOverlayEffectsComponent//, ICharacterUI
{
/// <summary>
/// An enum representing the current state being applied to the user
/// </summary>
private ScreenEffects _currentEffect = ScreenEffects.None;
#pragma warning disable 649
// Required dependencies
[Dependency] private readonly IOverlayManager _overlayManager;
[Dependency] private readonly IPlayerManager _playerManager;
#pragma warning restore 649
/// <summary>
/// Holds the screen effects that can be applied mapped ot their relevant overlay
/// </summary>
private Dictionary<ScreenEffects, Overlay> _effectsDictionary;
/// <summary>
/// Allows calculating if we need to act due to this component being controlled by the current mob
/// </summary>
private bool CurrentlyControlled => _playerManager.LocalPlayer.ControlledEntity == Owner;
public override void OnAdd()
{
base.OnAdd();
_effectsDictionary = new Dictionary<ScreenEffects, Overlay>()
{
{ ScreenEffects.CircleMask, new CircleMaskOverlay() },
{ ScreenEffects.GradientCircleMask, new GradientCircleMask() }
};
}
public override void HandleMessage(ComponentMessage message, INetChannel netChannel = null, IComponent component = null)
{
switch (message)
{
case PlayerAttachedMsg _:
SetOverlay(_currentEffect);
break;
}
}
public override void HandleComponentState(ComponentState curState, ComponentState nextState)
{
base.HandleComponentState(curState, nextState);
if (!(curState is OverlayEffectComponentState state) || _currentEffect == state.ScreenEffect) return;
SetOverlay(state.ScreenEffect);
}
private void SetOverlay(ScreenEffects effect)
{
RemoveOverlay();
_currentEffect = effect;
ApplyOverlay();
}
private void RemoveOverlay()
{
if (CurrentlyControlled && _currentEffect != ScreenEffects.None)
{
var appliedEffect = _effectsDictionary[_currentEffect];
_overlayManager.RemoveOverlay(appliedEffect.ID);
}
_currentEffect = ScreenEffects.None;
}
private void ApplyOverlay()
{
if (CurrentlyControlled && _currentEffect != ScreenEffects.None)
{
var overlay = _effectsDictionary[_currentEffect];
if (_overlayManager.HasOverlay(overlay.ID))
{
return;
}
_overlayManager.AddOverlay(overlay);
Logger.InfoS("overlay", $"Changed overlay to {overlay}");
}
}
}
}

View File

@@ -0,0 +1,116 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Client.UserInterface;
using Content.Client.Utility;
using Content.Shared.GameObjects.Components.Mobs;
using Robust.Client.GameObjects;
using Robust.Client.Interfaces.ResourceManagement;
using Robust.Client.Interfaces.UserInterface;
using Robust.Client.Player;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.ViewVariables;
namespace Content.Client.GameObjects.Components.Mobs
{
/// <inheritdoc/>
[RegisterComponent]
public sealed class ClientStatusEffectsComponent : SharedStatusEffectsComponent
{
#pragma warning disable 649
[Dependency] private readonly IPlayerManager _playerManager;
[Dependency] private readonly IResourceCache _resourceCache;
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager;
#pragma warning restore 649
private StatusEffectsUI _ui;
private IDictionary<StatusEffect, string> _icons = new Dictionary<StatusEffect, string>();
/// <summary>
/// Allows calculating if we need to act due to this component being controlled by the current mob
/// </summary>
private bool CurrentlyControlled => _playerManager.LocalPlayer.ControlledEntity == Owner;
protected override void Shutdown()
{
base.Shutdown();
PlayerDetached();
}
public override void HandleMessage(ComponentMessage message, INetChannel netChannel = null,
IComponent component = null)
{
base.HandleMessage(message, netChannel, component);
switch (message)
{
case PlayerAttachedMsg _:
PlayerAttached();
break;
case PlayerDetachedMsg _:
PlayerDetached();
break;
}
}
public override void HandleComponentState(ComponentState curState, ComponentState nextState)
{
base.HandleComponentState(curState, nextState);
if (!(curState is StatusEffectComponentState state) || _icons == state.StatusEffects) return;
_icons = state.StatusEffects;
UpdateIcons();
}
private void PlayerAttached()
{
if (!CurrentlyControlled || _ui != null)
{
return;
}
_ui = new StatusEffectsUI();
_userInterfaceManager.StateRoot.AddChild(_ui);
UpdateIcons();
}
private void PlayerDetached()
{
if (!CurrentlyControlled)
{
return;
}
_ui?.Dispose();
}
public void UpdateIcons()
{
if (!CurrentlyControlled || _ui == null)
{
return;
}
_ui.VBox.DisposeAllChildren();
foreach (var effect in _icons.OrderBy(x => (int) x.Key))
{
TextureRect newIcon = new TextureRect
{
TextureScale = (2, 2),
Texture = _resourceCache.GetTexture(effect.Value)
};
newIcon.Texture = _resourceCache.GetTexture(effect.Value);
_ui.VBox.AddChild(newIcon);
}
}
public void RemoveIcon(StatusEffect name)
{
_icons.Remove(name);
UpdateIcons();
Logger.InfoS("statuseffects", $"Removed icon {name}");
}
}
}

View File

@@ -1,171 +0,0 @@
using System.Collections.Generic;
using Content.Client.GameObjects.Components.Actor;
using Content.Client.Graphics.Overlays;
using Content.Client.UserInterface;
using Content.Client.Utility;
using Content.Shared.GameObjects;
using Content.Shared.GameObjects.Components.Mobs;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Graphics.Overlays;
using Robust.Client.Interfaces.Graphics.Overlays;
using Robust.Client.Interfaces.ResourceManagement;
using Robust.Client.Interfaces.UserInterface;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components.Renderable;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.IoC;
namespace Content.Client.GameObjects
{
/// <summary>
/// A character UI component which shows the current damage state of the mob (living/dead)
/// </summary>
[RegisterComponent]
public class SpeciesUI : SharedSpeciesComponent//, ICharacterUI
{
private StatusEffectsUI _ui;
/// <summary>
/// Holds the godot control for the species window
/// </summary>
private SpeciesWindow _window;
/// <summary>
/// An enum representing the current state being applied to the user
/// </summary>
private ScreenEffects _currentEffect = ScreenEffects.None;
#pragma warning disable 649
// Required dependencies
[Dependency] private readonly IOverlayManager _overlayManager;
[Dependency] private readonly IPlayerManager _playerManager;
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager;
[Dependency] private readonly IResourceCache _resourceCache;
#pragma warning restore 649
//Relevant interface implementation for the character UI controller
public Control Scene => _window;
public UIPriority Priority => UIPriority.Species;
/// <summary>
/// Allows calculating if we need to act due to this component being controlled by the current mob
/// </summary>
private bool CurrentlyControlled => _playerManager.LocalPlayer.ControlledEntity == Owner;
/// <summary>
/// Holds the screen effects that can be applied mapped ot their relevant overlay
/// </summary>
private Dictionary<ScreenEffects, Overlay> EffectsDictionary;
public override void OnRemove()
{
base.OnRemove();
_window.Dispose();
}
public override void OnAdd()
{
base.OnAdd();
_window = new SpeciesWindow();
_ui = new StatusEffectsUI();
EffectsDictionary = new Dictionary<ScreenEffects, Overlay>()
{
{ ScreenEffects.CircleMask, new CircleMaskOverlay() },
{ ScreenEffects.GradientCircleMask, new GradientCircleMask() }
};
}
public override void HandleMessage(ComponentMessage message, INetChannel netChannel = null, IComponent component = null)
{
switch (message)
{
case HudStateChange msg:
if (CurrentlyControlled)
{
ChangeHudIcon(msg);
}
break;
case PlayerAttachedMsg _:
_ui.Parent?.RemoveChild(_ui);
_userInterfaceManager.StateRoot.AddChild(_ui);
ApplyOverlay();
break;
case PlayerDetachedMsg _:
_ui.Parent?.RemoveChild(_ui);
RemoveOverlay();
break;
}
}
private void ChangeHudIcon(HudStateChange changeMessage)
{
var path = SharedSpriteComponent.TextureRoot / changeMessage.StateSprite;
var texture = _resourceCache.GetTexture(path);
_window.SetIcon(texture);
_ui.SetHealthIcon(texture);
SetOverlay(changeMessage);
}
private void SetOverlay(HudStateChange message)
{
RemoveOverlay();
_currentEffect = message.effect;
ApplyOverlay();
}
private void RemoveOverlay()
{
if (_currentEffect != ScreenEffects.None)
{
var appliedEffect = EffectsDictionary[_currentEffect];
_overlayManager.RemoveOverlay(appliedEffect.ID);
}
_currentEffect = ScreenEffects.None;
}
private void ApplyOverlay()
{
if (_currentEffect != ScreenEffects.None)
{
var overlay = EffectsDictionary[_currentEffect];
if (_overlayManager.HasOverlay(overlay.ID))
{
return;
}
_overlayManager.AddOverlay(overlay);
}
}
private class SpeciesWindow : TextureRect
{
public SpeciesWindow()
{
SizeFlagsHorizontal = SizeFlags.ShrinkCenter;
SizeFlagsVertical = SizeFlags.None;
Texture = IoCManager.Resolve<IResourceCache>().GetTexture("/Textures/Mob/UI/Human/human0.png");
}
public void SetIcon(Texture texture)
{
Texture = texture;
}
}
}
}

View File

@@ -12,29 +12,16 @@ namespace Content.Client.UserInterface
/// </summary>
public sealed class StatusEffectsUI : Control
{
public VBoxContainer VBox => _vBox;
private readonly VBoxContainer _vBox;
private TextureRect _healthStatusRect;
public StatusEffectsUI()
{
_vBox = new VBoxContainer {GrowHorizontal = GrowDirection.Begin};
AddChild(_vBox);
_vBox.AddChild(_healthStatusRect = new TextureRect
{
TextureScale = (2, 2),
Texture = IoCManager.Resolve<IResourceCache>().GetTexture("/Textures/Mob/UI/Human/human0.png")
});
SetAnchorAndMarginPreset(LayoutPreset.TopRight);
MarginTop = 250;
MarginRight = 10;
}
public void SetHealthIcon(Texture texture)
{
_healthStatusRect.Texture = texture;
}
}
}

View File

@@ -1,5 +1,7 @@
using System.Collections.Generic;
using Content.Shared.GameObjects;
using Content.Shared.GameObjects.Components.Mobs;
using Robust.Shared.Interfaces.GameObjects;
namespace Content.Server.GameObjects
{
@@ -13,10 +15,9 @@ namespace Content.Server.GameObjects
/// <summary>
/// Changes the hud state when a threshold is reached
/// </summary>
/// <param name="state"></param>
/// <param name="damage"></param>
/// <returns></returns>
public abstract HudStateChange ChangeHudState(DamageableComponent damage);
public abstract void ChangeHudState(DamageableComponent damage);
//public abstract ResistanceSet resistanceset { get; }

View File

@@ -1,8 +1,13 @@
using Content.Shared.GameObjects;
using System.Collections.Generic;
using Content.Server.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.Components.Mobs;
using JetBrains.Annotations;
using ScreenEffects = Content.Shared.GameObjects.Components.Mobs.ScreenEffects;
namespace Content.Server.GameObjects
{
[UsedImplicitly]
public class Human : DamageTemplates
{
int critvalue = 200;
@@ -30,9 +35,11 @@ namespace Content.Server.GameObjects
}
}
public override HudStateChange ChangeHudState(DamageableComponent damage)
public override void ChangeHudState(DamageableComponent damage)
{
ThresholdType healthstate = CalculateDamageState(damage);
damage.Owner.TryGetComponent(out ServerStatusEffectsComponent statusEffectsComponent);
damage.Owner.TryGetComponent(out ServerOverlayEffectsComponent overlayComponent);
switch (healthstate)
{
case ThresholdType.None:
@@ -42,23 +49,26 @@ namespace Content.Server.GameObjects
throw new System.InvalidOperationException(); //these should all be below the crit value, possibly going over multiple thresholds at once?
}
var modifier = totaldamage / (critvalue / normalstates); //integer division floors towards zero
return new HudStateChange()
{
StateSprite = "Mob/UI/Human/human" + modifier.ToString() + ".png",
effect = ScreenEffects.None
};
statusEffectsComponent?.ChangeStatus(StatusEffect.Health,
"/Textures/Mob/UI/Human/human" + modifier + ".png");
overlayComponent?.ChangeOverlay(ScreenEffects.None);
return;
case ThresholdType.Critical:
return new HudStateChange()
{
StateSprite = "Mob/UI/Human/humancrit-0.png", //TODO: display as gif or alternate with -0 and -1 as frames
effect = ScreenEffects.GradientCircleMask
};
statusEffectsComponent?.ChangeStatus(
StatusEffect.Health,
"/Textures/Mob/UI/Human/humancrit-0.png");
overlayComponent?.ChangeOverlay(ScreenEffects.GradientCircleMask);
return;
case ThresholdType.Death:
return new HudStateChange()
{
StateSprite = "Mob/UI/Human/humandead.png",
effect = ScreenEffects.CircleMask
};
statusEffectsComponent?.ChangeStatus(
StatusEffect.Health,
"/Textures/Mob/UI/Human/humandead.png");
overlayComponent?.ChangeOverlay(ScreenEffects.CircleMask);
return;
default:
throw new System.InvalidOperationException();
}

View File

@@ -0,0 +1,27 @@
using Content.Shared.GameObjects.Components.Mobs;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.Components.Mobs
{
[RegisterComponent]
[ComponentReference(typeof(SharedOverlayEffectsComponent))]
public sealed class ServerOverlayEffectsComponent : SharedOverlayEffectsComponent
{
private ScreenEffects _currentOverlay = ScreenEffects.None;
public override ComponentState GetComponentState()
{
return new OverlayEffectComponentState(_currentOverlay);
}
public void ChangeOverlay(ScreenEffects effect)
{
if (effect == _currentOverlay)
{
return;
}
_currentOverlay = effect;
Dirty();
}
}
}

View File

@@ -0,0 +1,40 @@
using System.Collections.Generic;
using Content.Shared.GameObjects.Components.Mobs;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.Components.Mobs
{
[RegisterComponent]
[ComponentReference(typeof(SharedStatusEffectsComponent))]
public sealed class ServerStatusEffectsComponent : SharedStatusEffectsComponent
{
private readonly Dictionary<StatusEffect, string> _statusEffects = new Dictionary<StatusEffect, string>();
public override ComponentState GetComponentState()
{
return new StatusEffectComponentState(_statusEffects);
}
public void ChangeStatus(StatusEffect effect, string icon)
{
if (_statusEffects.TryGetValue(effect, out string value) && value == icon)
{
return;
}
_statusEffects[effect] = icon;
Dirty();
}
public void RemoveStatus(StatusEffect effect)
{
if (!_statusEffects.Remove(effect))
{
return;
}
Dirty();
}
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.EntitySystems;
using Content.Server.Interfaces;
using Content.Shared.GameObjects.Components.Mobs;
@@ -56,12 +57,26 @@ namespace Content.Server.GameObjects
switch (message)
{
case PlayerAttachedMsg _:
var hudstatechange = DamageTemplate.ChangeHudState(Owner.GetComponent<DamageableComponent>());
SendNetworkMessage(hudstatechange);
if (CanReceiveStatusEffect(Owner)) {
DamageableComponent damage = Owner.GetComponent<DamageableComponent>();
DamageTemplate.ChangeHudState(damage);
}
break;
case PlayerDetachedMsg _:
break;
}
}
public override void OnRemove()
{
base.OnRemove();
Owner.TryGetComponent(out ServerStatusEffectsComponent statusEffectsComponent);
statusEffectsComponent?.RemoveStatus(StatusEffect.Health);
Owner.TryGetComponent(out ServerOverlayEffectsComponent overlayEffectsComponent);
overlayEffectsComponent?.ChangeOverlay(ScreenEffects.None);
}
bool IActionBlocker.CanMove()
{
return CurrentDamageState.CanMove();
@@ -103,14 +118,28 @@ namespace Content.Server.GameObjects
ChangeDamageState(DamageTemplate.CalculateDamageState(damage));
}
if (Owner.TryGetComponent(out BasicActorComponent actor)
) //specifies if we have a client to update the hud for
//specifies if we have a client to update the hud for
if (CanReceiveStatusEffect(Owner))
{
var hudstatechange = DamageTemplate.ChangeHudState(damage);
SendNetworkMessage(hudstatechange);
DamageTemplate.ChangeHudState(damage);
}
}
private bool CanReceiveStatusEffect(IEntity user)
{
if (!user.HasComponent<ServerStatusEffectsComponent>() &&
!user.HasComponent<ServerOverlayEffectsComponent>())
{
return false;
}
if (user.HasComponent<DamageableComponent>())
{
return true;
}
return false;
}
private void ChangeDamageState(ThresholdType threshold)
{
if (threshold == currentstate)

View File

@@ -0,0 +1,34 @@
using System;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization;
namespace Content.Shared.GameObjects.Components.Mobs
{
/// <summary>
/// Full screen overlays; Blindness, death, flash, alcohol etc.
/// </summary>
public abstract class SharedOverlayEffectsComponent : Component
{
public override string Name => "OverlayEffectsUI";
public sealed override uint? NetID => ContentNetIDs.OVERLAYEFFECTS;
public sealed override Type StateType => typeof(OverlayEffectComponentState);
}
public enum ScreenEffects
{
None,
CircleMask,
GradientCircleMask,
}
[Serializable, NetSerializable]
public class OverlayEffectComponentState : ComponentState
{
public ScreenEffects ScreenEffect;
public OverlayEffectComponentState(ScreenEffects screenEffect) : base(ContentNetIDs.OVERLAYEFFECTS)
{
ScreenEffect = screenEffect;
}
}
}

View File

@@ -8,8 +8,6 @@ namespace Content.Shared.GameObjects.Components.Mobs
{
public sealed override string Name => "Species";
public sealed override uint? NetID => ContentNetIDs.SPECIES;
[Serializable, NetSerializable]
public enum MobVisuals
{
@@ -29,6 +27,6 @@ namespace Content.Shared.GameObjects.Components.Mobs
/// </summary>
Down,
}
}
}

View File

@@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization;
namespace Content.Shared.GameObjects.Components.Mobs
{
/// <summary>
/// Handles the icons on the right side of the screen.
/// Should only be used for player-controlled entities
/// </summary>
public abstract class SharedStatusEffectsComponent : Component
{
public override string Name => "StatusEffectsUI";
public override uint? NetID => ContentNetIDs.STATUSEFFECTS;
public sealed override Type StateType => typeof(StatusEffectComponentState);
}
[Serializable, NetSerializable]
public class StatusEffectComponentState : ComponentState
{
public Dictionary<StatusEffect, string> StatusEffects;
public StatusEffectComponentState(Dictionary<StatusEffect, string> statusEffects) : base(ContentNetIDs.STATUSEFFECTS)
{
StatusEffects = statusEffects;
}
}
// Each status effect is assumed to be unique
public enum StatusEffect
{
Health,
}
}

View File

@@ -12,7 +12,6 @@
public const uint INVENTORY = 1006;
public const uint POWER_DEBUG_TOOL = 1007;
public const uint CONSTRUCTOR = 1008;
public const uint SPECIES = 1009;
public const uint RANGED_WEAPON = 1010;
public const uint CAMERA_RECOIL = 1011;
public const uint SOUND = 1012;
@@ -29,5 +28,7 @@
public const uint RESEARCH_CONSOLE = 1023;
public const uint WIRES = 1024;
public const uint COMBATMODE = 1025;
public const uint STATUSEFFECTS = 1026;
public const uint OVERLAYEFFECTS = 1027;
}
}

View File

@@ -1,28 +0,0 @@
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization;
using System;
namespace Content.Shared.GameObjects
{
/// <summary>
/// Sends updates to the standard species health hud with the sprite to change the hud to
/// </summary>
[Serializable, NetSerializable]
public class HudStateChange : ComponentMessage
{
public string StateSprite;
public ScreenEffects effect;
public HudStateChange()
{
Directed = true;
}
}
public enum ScreenEffects
{
None,
CircleMask,
GradientCircleMask,
}
}

View File

@@ -52,6 +52,8 @@
- type: Species
Template: Human
HeatResistance: 323
- type: StatusEffectsUI
- type: OverlayEffectsUI
- type: HeatResistance
- type: Damageable