From cb5acf7cd38ff566529a183c4313a0d8fcef5062 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Aguilera=20Puerto?= <6766154+Zumorica@users.noreply.github.com> Date: Fri, 12 Jun 2020 16:22:36 +0200 Subject: [PATCH] Cooldown for status effects (#1109) --- .../Mobs/ClientStatusEffectsComponent.cs | 73 +++++++++++++++---- .../EntitySystems/StatusEffectsSystem.cs | 34 +++++++++ .../DamageThresholdTemplates/HumanTemplate.cs | 6 +- .../Mobs/ServerStatusEffectsComponent.cs | 34 +++++++-- .../Components/Mobs/SpeciesComponent.cs | 2 +- .../Components/Mobs/StunnableComponent.cs | 57 +++++++-------- .../Components/Nutrition/HungerComponent.cs | 3 +- .../Components/Nutrition/ThirstComponent.cs | 2 +- .../Mobs/SharedStatusEffectsComponent.cs | 11 ++- 9 files changed, 163 insertions(+), 59 deletions(-) create mode 100644 Content.Client/GameObjects/EntitySystems/StatusEffectsSystem.cs diff --git a/Content.Client/GameObjects/Components/Mobs/ClientStatusEffectsComponent.cs b/Content.Client/GameObjects/Components/Mobs/ClientStatusEffectsComponent.cs index 9c177b31c9..632c3a4377 100644 --- a/Content.Client/GameObjects/Components/Mobs/ClientStatusEffectsComponent.cs +++ b/Content.Client/GameObjects/Components/Mobs/ClientStatusEffectsComponent.cs @@ -7,12 +7,15 @@ using Robust.Client.GameObjects; 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.Interfaces.GameObjects; using Robust.Shared.Interfaces.Network; +using Robust.Shared.Interfaces.Timing; using Robust.Shared.IoC; using Robust.Shared.Log; +using Robust.Shared.Maths; using Robust.Shared.Players; namespace Content.Client.GameObjects.Components.Mobs @@ -25,10 +28,12 @@ namespace Content.Client.GameObjects.Components.Mobs [Dependency] private readonly IPlayerManager _playerManager; [Dependency] private readonly IResourceCache _resourceCache; [Dependency] private readonly IUserInterfaceManager _userInterfaceManager; + [Dependency] private readonly IGameTiming _gameTiming; #pragma warning restore 649 private StatusEffectsUI _ui; - private IDictionary _icons = new Dictionary(); + private Dictionary _status = new Dictionary(); + private Dictionary _cooldown = new Dictionary(); /// /// Allows calculating if we need to act due to this component being controlled by the current mob @@ -58,9 +63,9 @@ namespace Content.Client.GameObjects.Components.Mobs 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(); + if (!(curState is StatusEffectComponentState state) || _status == state.StatusEffects) return; + _status = state.StatusEffects; + UpdateStatusEffects(); } private void PlayerAttached() @@ -71,7 +76,7 @@ namespace Content.Client.GameObjects.Components.Mobs } _ui = new StatusEffectsUI(); _userInterfaceManager.StateRoot.AddChild(_ui); - UpdateIcons(); + UpdateStatusEffects(); } private void PlayerDetached() @@ -80,32 +85,68 @@ namespace Content.Client.GameObjects.Components.Mobs _ui = null; } - public void UpdateIcons() + public void UpdateStatusEffects() { if (!CurrentlyControlled || _ui == null) { return; } + _cooldown.Clear(); _ui.VBox.DisposeAllChildren(); - foreach (var effect in _icons.OrderBy(x => (int) x.Key)) + foreach (var (key, statusEffect) in _status.OrderBy(x => (int) x.Key)) { - TextureRect newIcon = new TextureRect + var status = new Control() { - TextureScale = (2, 2), - Texture = _resourceCache.GetTexture(effect.Value) + Children = + { + new TextureRect + { + TextureScale = (2, 2), + Texture = _resourceCache.GetTexture(statusEffect.Icon) + }, + } }; - newIcon.Texture = _resourceCache.GetTexture(effect.Value); - _ui.VBox.AddChild(newIcon); + if (statusEffect.Cooldown.HasValue) + { + var cooldown = new CooldownGraphic(); + status.Children.Add(cooldown); + _cooldown[key] = cooldown; + } + + _ui.VBox.AddChild(status); } } - public void RemoveIcon(StatusEffect name) + public void RemoveStatusEffect(StatusEffect name) { - _icons.Remove(name); - UpdateIcons(); - Logger.InfoS("statuseffects", $"Removed icon {name}"); + _status.Remove(name); + UpdateStatusEffects(); + } + + public void FrameUpdate(float frameTime) + { + foreach (var (effect, cooldownGraphic) in _cooldown) + { + var status = _status[effect]; + if (!status.Cooldown.HasValue) + { + cooldownGraphic.Progress = 0; + cooldownGraphic.Visible = false; + continue; + } + + var start = status.Cooldown.Value.Item1; + var end = status.Cooldown.Value.Item2; + + var length = (end - start).TotalSeconds; + var progress = (_gameTiming.CurTime - start).TotalSeconds / length; + var ratio = (progress <= 1 ? (1 - progress) : (_gameTiming.CurTime - end).TotalSeconds * -5); + + cooldownGraphic.Progress = (float)ratio.Clamp(-1, 1); + cooldownGraphic.Visible = ratio > -1f; + } } } } diff --git a/Content.Client/GameObjects/EntitySystems/StatusEffectsSystem.cs b/Content.Client/GameObjects/EntitySystems/StatusEffectsSystem.cs new file mode 100644 index 0000000000..65476ce25e --- /dev/null +++ b/Content.Client/GameObjects/EntitySystems/StatusEffectsSystem.cs @@ -0,0 +1,34 @@ +using Content.Client.GameObjects.Components.Mobs; +using Content.Shared.GameObjects.Components.Mobs; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Interfaces.Timing; +using Robust.Shared.IoC; + +namespace Content.Client.GameObjects.EntitySystems +{ + public class StatusEffectsSystem : EntitySystem + { +#pragma warning disable 649 + [Dependency] private IGameTiming _gameTiming; +#pragma warning restore 649 + + public StatusEffectsSystem() + { + EntityQuery = new TypeEntityQuery(typeof(ClientStatusEffectsComponent)); + } + + public override void FrameUpdate(float frameTime) + { + base.FrameUpdate(frameTime); + + if (!_gameTiming.IsFirstTimePredicted) + return; + + foreach (var entity in RelevantEntities) + { + entity.GetComponent().FrameUpdate(frameTime); + } + } + } +} diff --git a/Content.Server/GameObjects/Components/Mobs/DamageThresholdTemplates/HumanTemplate.cs b/Content.Server/GameObjects/Components/Mobs/DamageThresholdTemplates/HumanTemplate.cs index a9d8ec77b7..18a93231da 100644 --- a/Content.Server/GameObjects/Components/Mobs/DamageThresholdTemplates/HumanTemplate.cs +++ b/Content.Server/GameObjects/Components/Mobs/DamageThresholdTemplates/HumanTemplate.cs @@ -66,21 +66,21 @@ namespace Content.Server.GameObjects throw new 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 - statusEffectsComponent?.ChangeStatus(StatusEffect.Health, + statusEffectsComponent?.ChangeStatusEffectIcon(StatusEffect.Health, "/Textures/Mob/UI/Human/human" + modifier + ".png"); overlayComponent?.ChangeOverlay(ScreenEffects.None); return; case ThresholdType.Critical: - statusEffectsComponent?.ChangeStatus( + statusEffectsComponent?.ChangeStatusEffectIcon( StatusEffect.Health, "/Textures/Mob/UI/Human/humancrit-0.png"); overlayComponent?.ChangeOverlay(ScreenEffects.GradientCircleMask); return; case ThresholdType.Death: - statusEffectsComponent?.ChangeStatus( + statusEffectsComponent?.ChangeStatusEffectIcon( StatusEffect.Health, "/Textures/Mob/UI/Human/humandead.png"); overlayComponent?.ChangeOverlay(ScreenEffects.CircleMask); diff --git a/Content.Server/GameObjects/Components/Mobs/ServerStatusEffectsComponent.cs b/Content.Server/GameObjects/Components/Mobs/ServerStatusEffectsComponent.cs index 646396d3e4..df70f1fee4 100644 --- a/Content.Server/GameObjects/Components/Mobs/ServerStatusEffectsComponent.cs +++ b/Content.Server/GameObjects/Components/Mobs/ServerStatusEffectsComponent.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using Content.Shared.GameObjects.Components.Mobs; using Robust.Shared.GameObjects; @@ -8,25 +9,48 @@ namespace Content.Server.GameObjects.Components.Mobs [ComponentReference(typeof(SharedStatusEffectsComponent))] public sealed class ServerStatusEffectsComponent : SharedStatusEffectsComponent { - private readonly Dictionary _statusEffects = new Dictionary(); + private readonly Dictionary _statusEffects = new Dictionary(); public override ComponentState GetComponentState() { return new StatusEffectComponentState(_statusEffects); } - public void ChangeStatus(StatusEffect effect, string icon) + public void ChangeStatusEffectIcon(StatusEffect effect, string icon) { - if (_statusEffects.TryGetValue(effect, out string value) && value == icon) + if (_statusEffects.TryGetValue(effect, out var value) && value.Icon == icon) { return; } - _statusEffects[effect] = icon; + _statusEffects[effect] = new StatusEffectStatus() + {Icon = icon, Cooldown = value.Cooldown}; Dirty(); } - public void RemoveStatus(StatusEffect effect) + public void ChangeStatusEffectCooldown(StatusEffect effect, ValueTuple cooldown) + { + if (_statusEffects.TryGetValue(effect, out var value) + && value.Cooldown == cooldown) + { + return; + } + + _statusEffects[effect] = new StatusEffectStatus() + { + Icon = value.Icon, Cooldown = cooldown + }; + Dirty(); + } + + public void ChangeStatusEffect(StatusEffect effect, string icon, ValueTuple? cooldown) + { + _statusEffects[effect] = new StatusEffectStatus() + {Icon = icon, Cooldown = cooldown}; + Dirty(); + } + + public void RemoveStatusEffect(StatusEffect effect) { if (!_statusEffects.Remove(effect)) { diff --git a/Content.Server/GameObjects/Components/Mobs/SpeciesComponent.cs b/Content.Server/GameObjects/Components/Mobs/SpeciesComponent.cs index d141742186..bae8e11772 100644 --- a/Content.Server/GameObjects/Components/Mobs/SpeciesComponent.cs +++ b/Content.Server/GameObjects/Components/Mobs/SpeciesComponent.cs @@ -75,7 +75,7 @@ namespace Content.Server.GameObjects { base.OnRemove(); Owner.TryGetComponent(out ServerStatusEffectsComponent statusEffectsComponent); - statusEffectsComponent?.RemoveStatus(StatusEffect.Health); + statusEffectsComponent?.RemoveStatusEffect(StatusEffect.Health); Owner.TryGetComponent(out ServerOverlayEffectsComponent overlayEffectsComponent); overlayEffectsComponent?.ChangeOverlay(ScreenEffects.None); diff --git a/Content.Server/GameObjects/Components/Mobs/StunnableComponent.cs b/Content.Server/GameObjects/Components/Mobs/StunnableComponent.cs index b2d5085248..e163b56cea 100644 --- a/Content.Server/GameObjects/Components/Mobs/StunnableComponent.cs +++ b/Content.Server/GameObjects/Components/Mobs/StunnableComponent.cs @@ -6,6 +6,7 @@ using Content.Server.Interfaces.GameObjects; using Content.Server.Mobs; using Content.Shared.Audio; using Content.Shared.GameObjects.Components.Mobs; +using Microsoft.Extensions.Logging; using Robust.Server.GameObjects; using Robust.Server.GameObjects.EntitySystems; using Robust.Shared.Audio; @@ -15,6 +16,7 @@ using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.Timers; using Robust.Shared.Interfaces.Timing; using Robust.Shared.IoC; +using Robust.Shared.Log; using Robust.Shared.Maths; using Robust.Shared.Serialization; using Robust.Shared.ViewVariables; @@ -39,7 +41,7 @@ namespace Content.Server.GameObjects.Components.Mobs [ViewVariables] public TimeSpan? StunEnd => _lastStun == null ? (TimeSpan?) null - : _gameTiming.CurTime + TimeSpan.FromSeconds(_stunnedTimer + _knockdownTimer + _slowdownTimer); + : _gameTiming.CurTime + (TimeSpan.FromSeconds(Math.Max(_stunnedTimer, Math.Max(_knockdownTimer, _slowdownTimer)))); private const int StunLevels = 8; @@ -56,7 +58,8 @@ namespace Content.Server.GameObjects.Components.Mobs private float _walkModifierOverride = 0f; private float _runModifierOverride = 0f; - private readonly string[] _texturesStunOverlay = new string[StunLevels]; + private string _stunTexture; + private CancellationTokenSource _statusRemoveCancellation = new CancellationTokenSource(); [ViewVariables] public bool Stunned => _stunnedTimer > 0f; [ViewVariables] public bool KnockedDown => _knockdownTimer > 0f; @@ -73,16 +76,7 @@ namespace Content.Server.GameObjects.Components.Mobs serializer.DataField(ref _slowdownCap, "slowdownCap", 20f); serializer.DataField(ref _helpInterval, "helpInterval", 1f); serializer.DataField(ref _helpKnockdownRemove, "helpKnockdownRemove", 1f); - } - - public override void Initialize() - { - base.Initialize(); - - for (var i = 0; i < StunLevels; i++) - { - _texturesStunOverlay[i] = $"/Textures/UserInterface/Inventory/cooldown-{i}.png"; - } + serializer.DataField(ref _stunTexture, "stunTexture", "/Textures/Objects/Melee/stunbaton.rsi/stunbaton_off.png"); } /// @@ -100,6 +94,8 @@ namespace Content.Server.GameObjects.Components.Mobs _stunnedTimer = seconds; _lastStun = _gameTiming.CurTime; + + SetStatusEffect(); } /// @@ -117,6 +113,8 @@ namespace Content.Server.GameObjects.Components.Mobs _knockdownTimer = seconds; _lastStun = _gameTiming.CurTime; + + SetStatusEffect(); } /// @@ -150,6 +148,8 @@ namespace Content.Server.GameObjects.Components.Mobs if(Owner.TryGetComponent(out MovementSpeedModifierComponent movement)) movement.RefreshMovementSpeedModifiers(); + + SetStatusEffect(); } /// @@ -174,9 +174,21 @@ namespace Content.Server.GameObjects.Components.Mobs _knockdownTimer -= _helpKnockdownRemove; + SetStatusEffect(); + return true; } + private void SetStatusEffect() + { + if (!Owner.TryGetComponent(out ServerStatusEffectsComponent status)) + return; + + status.ChangeStatusEffect(StatusEffect.Stun, _stunTexture, (StunStart == null || StunEnd == null) ? default : (StunStart.Value, StunEnd.Value)); + _statusRemoveCancellation.Cancel(); + _statusRemoveCancellation = new CancellationTokenSource(); + } + public void Update(float delta) { if (Stunned) @@ -214,33 +226,20 @@ namespace Content.Server.GameObjects.Components.Mobs } } - if (!_lastStun.HasValue || !StunEnd.HasValue || !Owner.TryGetComponent(out ServerStatusEffectsComponent status)) + if (!StunStart.HasValue || !StunEnd.HasValue || !Owner.TryGetComponent(out ServerStatusEffectsComponent status)) return; - var start = _lastStun.Value; + var start = StunStart.Value; var end = StunEnd.Value; var length = (end - start).TotalSeconds; var progress = (_gameTiming.CurTime - start).TotalSeconds; - var ratio = (float)(progress / length); - var textureIndex = CalculateStunLevel(ratio); - if (textureIndex == StunLevels) + if (progress >= length) { + Timer.Spawn(250, () => status.RemoveStatusEffect(StatusEffect.Stun), _statusRemoveCancellation.Token); _lastStun = null; - status.RemoveStatus(StatusEffect.Stun); } - else - { - status.ChangeStatus(StatusEffect.Stun, _texturesStunOverlay[textureIndex]); - } - } - - private static int CalculateStunLevel(float stunValue) - { - var val = stunValue.Clamp(0, 1); - val *= StunLevels; - return (int)Math.Floor(val); } #region ActionBlockers diff --git a/Content.Server/GameObjects/Components/Nutrition/HungerComponent.cs b/Content.Server/GameObjects/Components/Nutrition/HungerComponent.cs index 440fdc1b91..cd557b1ad4 100644 --- a/Content.Server/GameObjects/Components/Nutrition/HungerComponent.cs +++ b/Content.Server/GameObjects/Components/Nutrition/HungerComponent.cs @@ -78,8 +78,7 @@ namespace Content.Server.GameObjects.Components.Nutrition // Update UI Owner.TryGetComponent(out ServerStatusEffectsComponent statusEffectsComponent); - statusEffectsComponent?.ChangeStatus(StatusEffect.Hunger, _hungerThresholdImages[ (int)_currentHungerThreshold ]); - + statusEffectsComponent?.ChangeStatusEffectIcon(StatusEffect.Hunger, _hungerThresholdImages[ (int)_currentHungerThreshold ]); switch (_currentHungerThreshold) { case HungerThreshold.Overfed: diff --git a/Content.Server/GameObjects/Components/Nutrition/ThirstComponent.cs b/Content.Server/GameObjects/Components/Nutrition/ThirstComponent.cs index ae9e58571a..2d64123ef3 100644 --- a/Content.Server/GameObjects/Components/Nutrition/ThirstComponent.cs +++ b/Content.Server/GameObjects/Components/Nutrition/ThirstComponent.cs @@ -77,7 +77,7 @@ namespace Content.Server.GameObjects.Components.Nutrition // Update UI Owner.TryGetComponent(out ServerStatusEffectsComponent statusEffectsComponent); - statusEffectsComponent?.ChangeStatus(StatusEffect.Thirst, "/Textures/Mob/UI/Thirst/" + + statusEffectsComponent?.ChangeStatusEffectIcon(StatusEffect.Thirst, "/Textures/Mob/UI/Thirst/" + _currentThirstThreshold + ".png"); switch (_currentThirstThreshold) diff --git a/Content.Shared/GameObjects/Components/Mobs/SharedStatusEffectsComponent.cs b/Content.Shared/GameObjects/Components/Mobs/SharedStatusEffectsComponent.cs index 482b24fc17..3fd9e677fd 100644 --- a/Content.Shared/GameObjects/Components/Mobs/SharedStatusEffectsComponent.cs +++ b/Content.Shared/GameObjects/Components/Mobs/SharedStatusEffectsComponent.cs @@ -18,14 +18,21 @@ namespace Content.Shared.GameObjects.Components.Mobs [Serializable, NetSerializable] public class StatusEffectComponentState : ComponentState { - public Dictionary StatusEffects; + public Dictionary StatusEffects; - public StatusEffectComponentState(Dictionary statusEffects) : base(ContentNetIDs.STATUSEFFECTS) + public StatusEffectComponentState(Dictionary statusEffects) : base(ContentNetIDs.STATUSEFFECTS) { StatusEffects = statusEffects; } } + [Serializable, NetSerializable] + public struct StatusEffectStatus + { + public string Icon; + public ValueTuple? Cooldown; + } + // Each status effect is assumed to be unique public enum StatusEffect {