diff --git a/Content.Client/Construction/UI/ConstructionMenuPresenter.cs b/Content.Client/Construction/UI/ConstructionMenuPresenter.cs index f3b023e60d396c..ba786f27eac174 100644 --- a/Content.Client/Construction/UI/ConstructionMenuPresenter.cs +++ b/Content.Client/Construction/UI/ConstructionMenuPresenter.cs @@ -167,7 +167,7 @@ private void OnViewPopulateRecipes(object? sender, (string search, string catago continue; } - if (!string.IsNullOrEmpty(category) && category != Loc.GetString("construction-presenter-category-all")) + if (!string.IsNullOrEmpty(category) && category != Loc.GetString("construction-category-all")) { if (recipe.Category != category) continue; @@ -191,11 +191,11 @@ private void PopulateCategories() var uniqueCategories = new HashSet(); // hard-coded to show all recipes - uniqueCategories.Add(Loc.GetString("construction-presenter-category-all")); + uniqueCategories.Add(Loc.GetString("construction-category-all")); foreach (var prototype in _prototypeManager.EnumeratePrototypes()) { - var category = Loc.GetString(prototype.Category); + var category = prototype.Category; if (!string.IsNullOrEmpty(category)) uniqueCategories.Add(category); diff --git a/Content.Client/Damage/DamageVisualizer.cs b/Content.Client/Damage/DamageVisualizer.cs deleted file mode 100644 index 2c8c5286595a52..00000000000000 --- a/Content.Client/Damage/DamageVisualizer.cs +++ /dev/null @@ -1,857 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Content.Shared.Damage; -using Content.Shared.Damage.Prototypes; -using Content.Shared.FixedPoint; -using Robust.Client.GameObjects; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Log; -using Robust.Shared.Maths; -using Robust.Shared.Prototypes; -using Robust.Shared.Reflection; -using Robust.Shared.Serialization.Manager.Attributes; -using Robust.Shared.Utility; - -// apologies in advance for all the != null checks, -// my IDE wouldn't stop complaining about these - -namespace Content.Client.Damage -{ - /// - /// A simple visualizer for any entity with a DamageableComponent - /// to display the status of how damaged it is. - /// - /// Can either be an overlay for an entity, or target multiple - /// layers on the same entity. - /// - /// This can be disabled dynamically by passing into SetData, - /// key DamageVisualizerKeys.Disabled, value bool - /// (DamageVisualizerKeys lives in Content.Shared.Damage) - /// - /// Damage layers, if targetting layers, can also be dynamically - /// disabled if needed by passing into SetData, the name/enum - /// of the sprite layer, and then passing in a bool value - /// (true to enable, false to disable). - /// - public sealed class DamageVisualizer : AppearanceVisualizer - { - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly IEntityManager _entityManager = default!; - - private const string _name = "DamageVisualizer"; - /// - /// Damage thresholds between damage state changes. - /// - /// If there are any negative thresholds, or there is - /// less than one threshold, the visualizer is marked - /// as invalid. - /// - /// - /// A 'zeroth' threshold is automatically added, - /// and this list is automatically sorted for - /// efficiency beforehand. As such, the zeroth - /// threshold is not required - and negative - /// thresholds are automatically caught as - /// invalid. The zeroth threshold automatically - /// sets all layers to invisible, so a sprite - /// isn't required for it. - /// - [DataField("thresholds", required: true)] - private List _thresholds = new(); - - /// - /// Layers to target, by layerMapKey. - /// If a target layer map key is invalid - /// (in essence, undefined), then the target - /// layer is removed from the list for efficiency. - /// - /// If no layers are valid, then the visualizer - /// is marked as invalid. - /// - /// If this is not defined, however, the visualizer - /// instead adds an overlay to the sprite. - /// - /// - /// Layers can be disabled here by passing - /// the layer's name as a key to SetData, - /// and passing in a bool set to either 'false' - /// to disable it, or 'true' to enable it. - /// Setting the layer as disabled will make it - /// completely invisible. - /// - [DataField("targetLayers")] - private List? _targetLayers; - - /// - /// The actual sprites for every damage group - /// that the entity should display visually. - /// - /// This is keyed by a damage group identifier - /// (for example, Brute), and has a value - /// of a DamageVisualizerSprite (see below) - /// - [DataField("damageOverlayGroups")] - private readonly Dictionary? _damageOverlayGroups; - - /// - /// Sets if you want sprites to overlay the - /// entity when damaged, or if you would - /// rather have each target layer's state - /// replaced by a different state - /// within its RSI. - /// - /// This cannot be set to false if: - /// - There are no target layers - /// - There is no damage group - /// - [DataField("overlay")] - private readonly bool _overlay = true; - - /// - /// A single damage group to target. - /// This should only be defined if - /// overlay is set to false. - /// If this is defined with damageSprites, - /// this will be ignored. - /// - /// - /// This is here because otherwise, - /// you would need several permutations - /// of group sprites depending on - /// what kind of damage combination - /// you would want, on which threshold. - /// - [DataField("damageGroup")] - private readonly string? _damageGroup; - - /// - /// Set this if you want incoming damage to be - /// divided. - /// - /// - /// This is more useful if you have similar - /// damage sprites inbetween entities, - /// but with different damage thresholds - /// and you want to avoid duplicating - /// these sprites. - /// - [DataField("damageDivisor")] - private float _divisor = 1; - - /// - /// Set this to track all damage, instead of specific groups. - /// - /// - /// This will only work if you have damageOverlay - /// defined - otherwise, it will not work. - /// - [DataField("trackAllDamage")] - private readonly bool _trackAllDamage = false; - /// - /// This is the overlay sprite used, if _trackAllDamage is - /// enabled. Supports no complex per-group layering, - /// just an actually simple damage overlay. See - /// DamageVisualizerSprite for more information. - /// - [DataField("damageOverlay")] - private readonly DamageVisualizerSprite? _damageOverlay; - - // deals with the edge case of human damage visuals not - // being in color without making a Dict - /// The RSI path for the damage visualizer - /// group overlay. - /// - /// - /// States in here will require one of four - /// forms: - /// - /// If tracking damage groups: - /// - {base_state}_{group}_{threshold} if targetting - /// a static layer on a sprite (either as an - /// overlay or as a state change) - /// - DamageOverlay_{group}_{threshold} if not - /// targetting a layer on a sprite. - /// - /// If not tracking damage groups: - /// - {base_state}_{threshold} if it is targetting - /// a layer - /// - DamageOverlay_{threshold} if not targetting - /// a layer. - /// - [DataField("sprite", required: true)] - public readonly string Sprite = default!; - - /// - /// The color of this sprite overlay. - /// Supports only hexadecimal format. - /// - [DataField("color")] - public readonly string? Color; - } - - /// - /// Initializes an entity to be managed by this appearance controller. - /// DO NOT assume this is your only entity. Visualizers are shared. - /// - [Obsolete("Subscribe to your component being initialised instead.")] - public override void InitializeEntity(EntityUid entity) - { - base.InitializeEntity(entity); - - IoCManager.InjectDependencies(this); - - var damageData = _entityManager.EnsureComponent(entity); - VerifyVisualizerSetup(entity, damageData); - if (damageData.Valid) - InitializeVisualizer(entity, damageData); - } - - private void VerifyVisualizerSetup(EntityUid entity, DamageVisualizerDataComponent damageData) - { - if (_thresholds.Count < 1) - { - Logger.ErrorS(_name, $"Thresholds were invalid for entity {entity}. Thresholds: {_thresholds}"); - damageData.Valid = false; - return; - } - - if (_divisor == 0) - { - Logger.ErrorS(_name, $"Divisor for {entity} is set to zero."); - damageData.Valid = false; - return; - } - - if (_overlay) - { - if (_damageOverlayGroups == null && _damageOverlay == null) - { - Logger.ErrorS(_name, $"Enabled overlay without defined damage overlay sprites on {entity}."); - damageData.Valid = false; - return; - } - - if (_trackAllDamage && _damageOverlay == null) - { - Logger.ErrorS(_name, $"Enabled all damage tracking without a damage overlay sprite on {entity}."); - damageData.Valid = false; - return; - } - - if (!_trackAllDamage && _damageOverlay != null) - { - Logger.WarningS(_name, $"Disabled all damage tracking with a damage overlay sprite on {entity}."); - damageData.Valid = false; - return; - } - - - if (_trackAllDamage && _damageOverlayGroups != null) - { - Logger.WarningS(_name, $"Enabled all damage tracking with damage overlay groups on {entity}."); - damageData.Valid = false; - return; - } - } - else if (!_overlay) - { - if (_targetLayers == null) - { - Logger.ErrorS(_name, $"Disabled overlay without target layers on {entity}."); - damageData.Valid = false; - return; - } - - if (_damageOverlayGroups != null || _damageOverlay != null) - { - Logger.ErrorS(_name, $"Disabled overlay with defined damage overlay sprites on {entity}."); - damageData.Valid = false; - return; - } - - if (_damageGroup == null) - { - Logger.ErrorS(_name, $"Disabled overlay without defined damage group on {entity}."); - damageData.Valid = false; - return; - } - } - - if (_damageOverlayGroups != null && _damageGroup != null) - { - Logger.WarningS(_name, $"Damage overlay sprites and damage group are both defined on {entity}."); - } - - if (_damageOverlay != null && _damageGroup != null) - { - Logger.WarningS(_name, $"Damage overlay sprites and damage group are both defined on {entity}."); - } - } - - private void InitializeVisualizer(EntityUid entity, DamageVisualizerDataComponent damageData) - { - if (!_entityManager.TryGetComponent(entity, out SpriteComponent? spriteComponent) - || !_entityManager.TryGetComponent(entity, out var damageComponent) - || !_entityManager.HasComponent(entity)) - return; - - _thresholds.Add(FixedPoint2.Zero); - _thresholds.Sort(); - - if (_thresholds[0] != 0) - { - Logger.ErrorS(_name, $"Thresholds were invalid for entity {entity}. Thresholds: {_thresholds}"); - damageData.Valid = false; - return; - } - - // If the damage container on our entity's DamageableComponent - // is not null, we can try to check through its groups. - if (damageComponent.DamageContainerID != null - && _prototypeManager.TryIndex(damageComponent.DamageContainerID, out var damageContainer)) - { - // Are we using damage overlay sprites by group? - // Check if the container matches the supported groups, - // and start cacheing the last threshold. - if (_damageOverlayGroups != null) - { - foreach (string damageType in _damageOverlayGroups.Keys) - { - if (!damageContainer.SupportedGroups.Contains(damageType)) - { - Logger.ErrorS(_name, $"Damage key {damageType} was invalid for entity {entity}."); - damageData.Valid = false; - return; - } - - damageData.LastThresholdPerGroup.Add(damageType, FixedPoint2.Zero); - } - } - // Are we tracking a single damage group without overlay instead? - // See if that group is in our entity's damage container. - else if (!_overlay && _damageGroup != null) - { - if (!damageContainer.SupportedGroups.Contains(_damageGroup)) - { - Logger.ErrorS(_name, $"Damage keys were invalid for entity {entity}."); - damageData.Valid = false; - return; - } - - damageData.LastThresholdPerGroup.Add(_damageGroup, FixedPoint2.Zero); - } - } - // Ditto above, but instead we go through every group. - else // oh boy! time to enumerate through every single group! - { - var damagePrototypeIdList = _prototypeManager.EnumeratePrototypes() - .Select((p, _) => p.ID) - .ToList(); - if (_damageOverlayGroups != null) - foreach (string damageType in _damageOverlayGroups.Keys) - { - if (!damagePrototypeIdList.Contains(damageType)) - { - Logger.ErrorS(_name, $"Damage keys were invalid for entity {entity}."); - damageData.Valid = false; - return; - } - damageData.LastThresholdPerGroup.Add(damageType, FixedPoint2.Zero); - } - else if (_damageGroup != null) - { - if (!damagePrototypeIdList.Contains(_damageGroup)) - { - Logger.ErrorS(_name, $"Damage keys were invalid for entity {entity}."); - damageData.Valid = false; - return; - } - - damageData.LastThresholdPerGroup.Add(_damageGroup, FixedPoint2.Zero); - } - } - - // If we're targetting any layers, and the amount of - // layers is greater than zero, we start reserving - // all the layers needed to track damage groups - // on the entity. - if (_targetLayers != null && _targetLayers.Count > 0) - { - // This should ensure that the layers we're targetting - // are valid for the visualizer's use. - // - // If the layer doesn't have a base state, or - // the layer key just doesn't exist, we skip it. - foreach (var key in _targetLayers) - { - if (!spriteComponent.LayerMapTryGet(key, out int index) - || spriteComponent.LayerGetState(index).ToString() == null) - { - Logger.WarningS(_name, $"Layer at key {key} was invalid for entity {entity}."); - continue; - } - - damageData.TargetLayerMapKeys.Add(key); - }; - - // Similar to damage overlay groups, if none of the targetted - // sprite layers could be used, we display an error and - // invalidate the visualizer without crashing. - if (damageData.TargetLayerMapKeys.Count == 0) - { - Logger.ErrorS(_name, $"Target layers were invalid for entity {entity}."); - damageData.Valid = false; - return; - } - - // Otherwise, we start reserving layers. Since the filtering - // loop above ensures that all of these layers are not null, - // and have valid state IDs, there should be no issues. - foreach (object layer in damageData.TargetLayerMapKeys) - { - int layerCount = spriteComponent.AllLayers.Count(); - int index = spriteComponent.LayerMapGet(layer); - string layerState = spriteComponent.LayerGetState(index)!.ToString()!; - - if (index + 1 != layerCount) - { - index += 1; - } - - damageData.LayerMapKeyStates.Add(layer, layerState); - - // If we're an overlay, and we're targetting groups, - // we reserve layers per damage group. - if (_overlay && _damageOverlayGroups != null) - { - foreach (var (group, sprite) in _damageOverlayGroups) - { - AddDamageLayerToSprite(spriteComponent, - sprite, - $"{layerState}_{group}_{_thresholds[1]}", - $"{layer}{group}", - index); - } - damageData.DisabledLayers.Add(layer, false); - } - // If we're not targetting groups, and we're still - // using an overlay, we instead just add a general - // overlay that reflects on how much damage - // was taken. - else if (_damageOverlay != null) - { - AddDamageLayerToSprite(spriteComponent, - _damageOverlay, - $"{layerState}_{_thresholds[1]}", - $"{layer}trackDamage", - index); - damageData.DisabledLayers.Add(layer, false); - } - } - } - // If we're not targetting layers, however, - // we should ensure that we instead - // reserve it as an overlay. - else - { - if (_damageOverlayGroups != null) - { - foreach (var (group, sprite) in _damageOverlayGroups) - { - AddDamageLayerToSprite(spriteComponent, - sprite, - $"DamageOverlay_{group}_{_thresholds[1]}", - $"DamageOverlay{group}"); - damageData.TopMostLayerKey = $"DamageOverlay{group}"; - } - } - else if (_damageOverlay != null) - { - AddDamageLayerToSprite(spriteComponent, - _damageOverlay, - $"DamageOverlay_{_thresholds[1]}", - "DamageOverlay"); - damageData.TopMostLayerKey = $"DamageOverlay"; - } - } - } - - /// - /// Adds a damage tracking layer to a given sprite component. - /// - private void AddDamageLayerToSprite(SpriteComponent spriteComponent, DamageVisualizerSprite sprite, string state, string mapKey, int? index = null) - { - int newLayer = spriteComponent.AddLayer( - new SpriteSpecifier.Rsi( - new ResourcePath(sprite.Sprite), state - ), index); - spriteComponent.LayerMapSet(mapKey, newLayer); - if (sprite.Color != null) - spriteComponent.LayerSetColor(newLayer, Color.FromHex(sprite.Color)); - spriteComponent.LayerSetVisible(newLayer, false); - } - - [Obsolete("Subscribe to AppearanceChangeEvent instead.")] - public override void OnChangeData(AppearanceComponent component) - { - var entities = _entityManager; - if (!entities.TryGetComponent(component.Owner, out DamageVisualizerDataComponent? damageData)) - return; - - if (!damageData.Valid) - return; - - // If this was passed into the component, we update - // the data to ensure that the current disabled - // bool matches. - if (component.TryGetData(DamageVisualizerKeys.Disabled, out var disabledStatus)) - if (disabledStatus != damageData.Disabled) - damageData.Disabled = disabledStatus; - - if (damageData.Disabled) - return; - - HandleDamage(component, damageData); - } - - private void HandleDamage(AppearanceComponent component, DamageVisualizerDataComponent damageData) - { - var entities = _entityManager; - if (!entities.TryGetComponent(component.Owner, out SpriteComponent? spriteComponent) - || !entities.TryGetComponent(component.Owner, out DamageableComponent? damageComponent)) - return; - - if (_targetLayers != null && _damageOverlayGroups != null) - UpdateDisabledLayers(spriteComponent, component, damageData); - - if (_overlay && _damageOverlayGroups != null && _targetLayers == null) - CheckOverlayOrdering(spriteComponent, damageData); - - if (component.TryGetData(DamageVisualizerKeys.ForceUpdate, out bool update) - && update) - { - ForceUpdateLayers(damageComponent, spriteComponent, damageData); - return; - } - - if (_trackAllDamage) - { - UpdateDamageVisuals(damageComponent, spriteComponent, damageData); - } - else if (component.TryGetData(DamageVisualizerKeys.DamageUpdateGroups, out DamageVisualizerGroupData data)) - { - UpdateDamageVisuals(data.GroupList, damageComponent, spriteComponent, damageData); - } - } - - /// - /// Checks if any layers were disabled in the last - /// data update. Disabled layers mean that the - /// layer will no longer be visible, or obtain - /// any damage updates. - /// - private void UpdateDisabledLayers(SpriteComponent spriteComponent, AppearanceComponent component, DamageVisualizerDataComponent damageData) - { - foreach (var layer in damageData.TargetLayerMapKeys) - { - bool? layerStatus = null; - if (component.TryGetData(layer, out var layerStateEnum)) - layerStatus = layerStateEnum; - - if (layerStatus == null) - continue; - - if (damageData.DisabledLayers[layer] != (bool) layerStatus) - { - damageData.DisabledLayers[layer] = (bool) layerStatus; - if (!_trackAllDamage && _damageOverlayGroups != null) - foreach (string damageGroup in _damageOverlayGroups!.Keys) - spriteComponent.LayerSetVisible($"{layer}{damageGroup}", damageData.DisabledLayers[layer]); - else if (_trackAllDamage) - spriteComponent.LayerSetVisible($"{layer}trackDamage", damageData.DisabledLayers[layer]); - } - } - } - - /// - /// Checks the overlay ordering on the current - /// sprite component, compared to the - /// data for the visualizer. If the top - /// most layer doesn't match, the sprite - /// layers are recreated and placed on top. - /// - private void CheckOverlayOrdering(SpriteComponent spriteComponent, DamageVisualizerDataComponent damageData) - { - if (spriteComponent[damageData.TopMostLayerKey] != spriteComponent[spriteComponent.AllLayers.Count() - 1]) - { - if (!_trackAllDamage && _damageOverlayGroups != null) - { - foreach (var (damageGroup, sprite) in _damageOverlayGroups) - { - FixedPoint2 threshold = damageData.LastThresholdPerGroup[damageGroup]; - ReorderOverlaySprite(spriteComponent, - damageData, - sprite, - $"DamageOverlay{damageGroup}", - $"DamageOverlay_{damageGroup}", - threshold); - } - } - else if (_trackAllDamage && _damageOverlay != null) - { - ReorderOverlaySprite(spriteComponent, - damageData, - _damageOverlay, - $"DamageOverlay", - $"DamageOverlay", - damageData.LastDamageThreshold); - } - } - } - - private void ReorderOverlaySprite(SpriteComponent spriteComponent, DamageVisualizerDataComponent damageData, DamageVisualizerSprite sprite, string key, string statePrefix, FixedPoint2 threshold) - { - spriteComponent.LayerMapTryGet(key, out int spriteLayer); - bool visibility = spriteComponent[spriteLayer].Visible; - spriteComponent.RemoveLayer(spriteLayer); - if (threshold == FixedPoint2.Zero) // these should automatically be invisible - threshold = _thresholds[1]; - spriteLayer = spriteComponent.AddLayer( - new SpriteSpecifier.Rsi( - new ResourcePath(sprite.Sprite), - $"{statePrefix}_{threshold}" - ), - spriteLayer); - spriteComponent.LayerMapSet(key, spriteLayer); - spriteComponent.LayerSetVisible(spriteLayer, visibility); - // this is somewhat iffy since it constantly reallocates - damageData.TopMostLayerKey = key; - } - - /// - /// Updates damage visuals without tracking - /// any damage groups. - /// - private void UpdateDamageVisuals(DamageableComponent damageComponent, SpriteComponent spriteComponent, DamageVisualizerDataComponent damageData) - { - if (!CheckThresholdBoundary(damageComponent.TotalDamage, damageData.LastDamageThreshold, out FixedPoint2 threshold)) - return; - - damageData.LastDamageThreshold = threshold; - - if (_targetLayers != null) - { - foreach (var layerMapKey in damageData.TargetLayerMapKeys) - UpdateTargetLayer(spriteComponent, damageData, layerMapKey, threshold); - } - else - { - UpdateOverlay(spriteComponent, threshold); - } - } - - /// - /// Updates damage visuals by damage group, - /// according to the list of damage groups - /// passed into it. - /// - private void UpdateDamageVisuals(List delta, DamageableComponent damageComponent, SpriteComponent spriteComponent, DamageVisualizerDataComponent damageData) - { - foreach (var damageGroup in delta) - { - if (!_overlay && damageGroup != _damageGroup) - continue; - - if (!_prototypeManager.TryIndex(damageGroup, out var damageGroupPrototype) - || !damageComponent.Damage.TryGetDamageInGroup(damageGroupPrototype, out FixedPoint2 damageTotal)) - continue; - - if (!damageData.LastThresholdPerGroup.TryGetValue(damageGroup, out FixedPoint2 lastThreshold) - || !CheckThresholdBoundary(damageTotal, lastThreshold, out FixedPoint2 threshold)) - continue; - - damageData.LastThresholdPerGroup[damageGroup] = threshold; - - if (_targetLayers != null) - { - foreach (var layerMapKey in damageData.TargetLayerMapKeys) - UpdateTargetLayer(spriteComponent, damageData, layerMapKey, damageGroup, threshold); - } - else - { - UpdateOverlay(spriteComponent, damageGroup, threshold); - } - } - - } - - /// - /// Checks if a threshold boundary was passed. - /// - private bool CheckThresholdBoundary(FixedPoint2 damageTotal, FixedPoint2 lastThreshold, out FixedPoint2 threshold) - { - threshold = FixedPoint2.Zero; - damageTotal = damageTotal / _divisor; - int thresholdIndex = _thresholds.BinarySearch(damageTotal); - - if (thresholdIndex < 0) - { - thresholdIndex = ~thresholdIndex; - threshold = _thresholds[thresholdIndex - 1]; - } - else - { - threshold = _thresholds[thresholdIndex]; - } - - if (threshold == lastThreshold) - return false; - - return true; - } - - /// - /// This is the entry point for - /// forcing an update on all damage layers. - /// Does different things depending on - /// the configuration of the visualizer. - /// - private void ForceUpdateLayers(DamageableComponent damageComponent, SpriteComponent spriteComponent, DamageVisualizerDataComponent damageData) - { - if (_damageOverlayGroups != null) - { - UpdateDamageVisuals(_damageOverlayGroups.Keys.ToList(), damageComponent, spriteComponent, damageData); - } - else if (_damageGroup != null) - { - UpdateDamageVisuals(new List(){ _damageGroup }, damageComponent, spriteComponent, damageData); - } - else if (_damageOverlay != null) - { - UpdateDamageVisuals(damageComponent, spriteComponent, damageData); - } - } - - /// - /// Updates a target layer. Without a damage group passed in, - /// it assumes you're updating a layer that is tracking all - /// damage. - /// - private void UpdateTargetLayer(SpriteComponent spriteComponent, DamageVisualizerDataComponent damageData, object layerMapKey, FixedPoint2 threshold) - { - if (_overlay && _damageOverlayGroups != null) - { - if (!damageData.DisabledLayers[layerMapKey]) - { - string layerState = damageData.LayerMapKeyStates[layerMapKey]; - spriteComponent.LayerMapTryGet($"{layerMapKey}trackDamage", out int spriteLayer); - - UpdateDamageLayerState(spriteComponent, - spriteLayer, - $"{layerState}", - threshold); - } - } - else if (!_overlay) - { - string layerState = damageData.LayerMapKeyStates[layerMapKey]; - spriteComponent.LayerMapTryGet(layerMapKey, out int spriteLayer); - - UpdateDamageLayerState(spriteComponent, - spriteLayer, - $"{layerState}", - threshold); - } - } - - /// - /// Updates a target layer by damage group. - /// - private void UpdateTargetLayer(SpriteComponent spriteComponent, DamageVisualizerDataComponent damageData, object layerMapKey, string damageGroup, FixedPoint2 threshold) - { - if (_overlay && _damageOverlayGroups != null) - { - if (_damageOverlayGroups.ContainsKey(damageGroup) && !damageData.DisabledLayers[layerMapKey]) - { - string layerState = damageData.LayerMapKeyStates[layerMapKey]; - spriteComponent.LayerMapTryGet($"{layerMapKey}{damageGroup}", out int spriteLayer); - - UpdateDamageLayerState(spriteComponent, - spriteLayer, - $"{layerState}_{damageGroup}", - threshold); - } - } - else if (!_overlay) - { - string layerState = damageData.LayerMapKeyStates[layerMapKey]; - spriteComponent.LayerMapTryGet(layerMapKey, out int spriteLayer); - - UpdateDamageLayerState(spriteComponent, - spriteLayer, - $"{layerState}_{damageGroup}", - threshold); - } - } - - /// - /// Updates an overlay that is tracking all damage. - /// - private void UpdateOverlay(SpriteComponent spriteComponent, FixedPoint2 threshold) - { - spriteComponent.LayerMapTryGet($"DamageOverlay", out int spriteLayer); - - UpdateDamageLayerState(spriteComponent, - spriteLayer, - $"DamageOverlay", - threshold); - } - - /// - /// Updates an overlay based on damage group. - /// - private void UpdateOverlay(SpriteComponent spriteComponent, string damageGroup, FixedPoint2 threshold) - { - if (_damageOverlayGroups != null) - { - if (_damageOverlayGroups.ContainsKey(damageGroup)) - { - spriteComponent.LayerMapTryGet($"DamageOverlay{damageGroup}", out int spriteLayer); - - UpdateDamageLayerState(spriteComponent, - spriteLayer, - $"DamageOverlay_{damageGroup}", - threshold); - } - } - } - - /// - /// Updates a layer on the sprite by what - /// prefix it has (calculated by whatever - /// function calls it), and what threshold - /// was passed into it. - /// - private void UpdateDamageLayerState(SpriteComponent spriteComponent, int spriteLayer, string statePrefix, FixedPoint2 threshold) - { - if (threshold == 0) - { - spriteComponent.LayerSetVisible(spriteLayer, false); - } - else - { - if (!spriteComponent[spriteLayer].Visible) - { - spriteComponent.LayerSetVisible(spriteLayer, true); - } - spriteComponent.LayerSetState(spriteLayer, $"{statePrefix}_{threshold}"); - } - } - } -} diff --git a/Content.Client/Damage/DamageVisualizerComponent.cs b/Content.Client/Damage/DamageVisualizerComponent.cs deleted file mode 100644 index 984cf0e2aa7ccf..00000000000000 --- a/Content.Client/Damage/DamageVisualizerComponent.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Collections.Generic; -using Content.Shared.FixedPoint; -using Robust.Shared.GameObjects; - -namespace Content.Client.Damage -{ - // Stores all the data for a DamageVisualizer. - // - // Storing it inside of the AppearanceComponent's data - // dictionary was too messy, but at least we can - // store it in the entity itself as a separate, - // dynamically added component. - [RegisterComponent] - public sealed class DamageVisualizerDataComponent : Component - { - public List TargetLayerMapKeys = new(); - public bool Disabled = false; - public bool Valid = true; - public FixedPoint2 LastDamageThreshold = FixedPoint2.Zero; - public Dictionary DisabledLayers = new(); - public Dictionary LayerMapKeyStates = new(); - public Dictionary LastThresholdPerGroup = new(); - public string TopMostLayerKey = default!; - } -} diff --git a/Content.Client/Damage/DamageVisualsComponent.cs b/Content.Client/Damage/DamageVisualsComponent.cs new file mode 100644 index 00000000000000..1ccf7a5c11deda --- /dev/null +++ b/Content.Client/Damage/DamageVisualsComponent.cs @@ -0,0 +1,161 @@ +using Content.Shared.FixedPoint; + +namespace Content.Client.Damage; + +[RegisterComponent] +public sealed class DamageVisualsComponent : Component +{ + /// + /// Damage thresholds between damage state changes. + /// + /// If there are any negative thresholds, or there is + /// less than one threshold, the visualizer is marked + /// as invalid. + /// + /// + /// A 'zeroth' threshold is automatically added, + /// and this list is automatically sorted for + /// efficiency beforehand. As such, the zeroth + /// threshold is not required - and negative + /// thresholds are automatically caught as + /// invalid. The zeroth threshold automatically + /// sets all layers to invisible, so a sprite + /// isn't required for it. + /// + [DataField("thresholds", required: true)] + public List Thresholds = new(); + + /// + /// Layers to target, by layerMapKey. + /// If a target layer map key is invalid + /// (in essence, undefined), then the target + /// layer is removed from the list for efficiency. + /// + /// If no layers are valid, then the visualizer + /// is marked as invalid. + /// + /// If this is not defined, however, the visualizer + /// instead adds an overlay to the sprite. + /// + /// + /// Layers can be disabled here by passing + /// the layer's name as a key to SetData, + /// and passing in a bool set to either 'false' + /// to disable it, or 'true' to enable it. + /// Setting the layer as disabled will make it + /// completely invisible. + /// + [DataField("targetLayers")] public List? TargetLayers; + + /// + /// The actual sprites for every damage group + /// that the entity should display visually. + /// + /// This is keyed by a damage group identifier + /// (for example, Brute), and has a value + /// of a DamageVisualizerSprite (see below) + /// + [DataField("damageOverlayGroups")] public readonly Dictionary? DamageOverlayGroups; + + /// + /// Sets if you want sprites to overlay the + /// entity when damaged, or if you would + /// rather have each target layer's state + /// replaced by a different state + /// within its RSI. + /// + /// This cannot be set to false if: + /// - There are no target layers + /// - There is no damage group + /// + [DataField("overlay")] public readonly bool Overlay = true; + + /// + /// A single damage group to target. + /// This should only be defined if + /// overlay is set to false. + /// If this is defined with damageSprites, + /// this will be ignored. + /// + /// + /// This is here because otherwise, + /// you would need several permutations + /// of group sprites depending on + /// what kind of damage combination + /// you would want, on which threshold. + /// + [DataField("damageGroup")] public readonly string? DamageGroup; + + /// + /// Set this if you want incoming damage to be + /// divided. + /// + /// + /// This is more useful if you have similar + /// damage sprites in between entities, + /// but with different damage thresholds + /// and you want to avoid duplicating + /// these sprites. + /// + [DataField("damageDivisor")] public float Divisor = 1; + + /// + /// Set this to track all damage, instead of specific groups. + /// + /// + /// This will only work if you have damageOverlay + /// defined - otherwise, it will not work. + /// + [DataField("trackAllDamage")] public readonly bool TrackAllDamage; + /// + /// This is the overlay sprite used, if _trackAllDamage is + /// enabled. Supports no complex per-group layering, + /// just an actually simple damage overlay. See + /// DamageVisualizerSprite for more information. + /// + [DataField("damageOverlay")] public readonly DamageVisualizerSprite? DamageOverlay; + + public readonly List TargetLayerMapKeys = new(); + public bool Disabled = false; + public bool Valid = true; + public FixedPoint2 LastDamageThreshold = FixedPoint2.Zero; + public readonly Dictionary DisabledLayers = new(); + public readonly Dictionary LayerMapKeyStates = new(); + public readonly Dictionary LastThresholdPerGroup = new(); + public string TopMostLayerKey = default!; +} + +// deals with the edge case of human damage visuals not +// being in color without making a Dict + /// The RSI path for the damage visualizer + /// group overlay. + /// + /// + /// States in here will require one of four + /// forms: + /// + /// If tracking damage groups: + /// - {base_state}_{group}_{threshold} if targeting + /// a static layer on a sprite (either as an + /// overlay or as a state change) + /// - DamageOverlay_{group}_{threshold} if not + /// targeting a layer on a sprite. + /// + /// If not tracking damage groups: + /// - {base_state}_{threshold} if it is targeting + /// a layer + /// - DamageOverlay_{threshold} if not targeting + /// a layer. + /// + [DataField("sprite", required: true)] public readonly string Sprite = default!; + + /// + /// The color of this sprite overlay. + /// Supports only hexadecimal format. + /// + [DataField("color")] public readonly string? Color; +} diff --git a/Content.Client/Damage/DamageVisualsSystem.cs b/Content.Client/Damage/DamageVisualsSystem.cs new file mode 100644 index 00000000000000..adf245e4a531bc --- /dev/null +++ b/Content.Client/Damage/DamageVisualsSystem.cs @@ -0,0 +1,698 @@ +using System.Linq; +using Content.Shared.Damage; +using Content.Shared.Damage.Prototypes; +using Content.Shared.FixedPoint; +using Robust.Client.GameObjects; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.Client.Damage; + +/// +/// A simple visualizer for any entity with a DamageableComponent +/// to display the status of how damaged it is. +/// +/// Can either be an overlay for an entity, or target multiple +/// layers on the same entity. +/// +/// This can be disabled dynamically by passing into SetData, +/// key DamageVisualizerKeys.Disabled, value bool +/// (DamageVisualizerKeys lives in Content.Shared.Damage) +/// +/// Damage layers, if targeting layers, can also be dynamically +/// disabled if needed by passing into SetData, the name/enum +/// of the sprite layer, and then passing in a bool value +/// (true to enable, false to disable). +/// +public sealed class DamageVisualsSystem : VisualizerSystem +{ + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + + private const string SawmillName = "DamageVisuals"; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(InitializeEntity); + } + + private void InitializeEntity(EntityUid entity, DamageVisualsComponent comp, ComponentInit args) + { + VerifyVisualizerSetup(entity, comp); + + if (!comp.Valid) + { + RemCompDeferred(entity); + return; + } + + InitializeVisualizer(entity, comp); + } + + private void VerifyVisualizerSetup(EntityUid entity, DamageVisualsComponent damageVisComp) + { + if (damageVisComp.Thresholds.Count < 1) + { + Logger.ErrorS(SawmillName, $"Thresholds were invalid for entity {entity}. Thresholds: {damageVisComp.Thresholds}"); + damageVisComp.Valid = false; + return; + } + + if (damageVisComp.Divisor == 0) + { + Logger.ErrorS(SawmillName, $"Divisor for {entity} is set to zero."); + damageVisComp.Valid = false; + return; + } + + if (damageVisComp.Overlay) + { + if (damageVisComp.DamageOverlayGroups == null && damageVisComp.DamageOverlay == null) + { + Logger.ErrorS(SawmillName, $"Enabled overlay without defined damage overlay sprites on {entity}."); + damageVisComp.Valid = false; + return; + } + + if (damageVisComp.TrackAllDamage && damageVisComp.DamageOverlay == null) + { + Logger.ErrorS(SawmillName, $"Enabled all damage tracking without a damage overlay sprite on {entity}."); + damageVisComp.Valid = false; + return; + } + + if (!damageVisComp.TrackAllDamage && damageVisComp.DamageOverlay != null) + { + Logger.WarningS(SawmillName, $"Disabled all damage tracking with a damage overlay sprite on {entity}."); + damageVisComp.Valid = false; + return; + } + + + if (damageVisComp.TrackAllDamage && damageVisComp.DamageOverlayGroups != null) + { + Logger.WarningS(SawmillName, $"Enabled all damage tracking with damage overlay groups on {entity}."); + damageVisComp.Valid = false; + return; + } + } + else if (!damageVisComp.Overlay) + { + if (damageVisComp.TargetLayers == null) + { + Logger.ErrorS(SawmillName, $"Disabled overlay without target layers on {entity}."); + damageVisComp.Valid = false; + return; + } + + if (damageVisComp.DamageOverlayGroups != null || damageVisComp.DamageOverlay != null) + { + Logger.ErrorS(SawmillName, $"Disabled overlay with defined damage overlay sprites on {entity}."); + damageVisComp.Valid = false; + return; + } + + if (damageVisComp.DamageGroup == null) + { + Logger.ErrorS(SawmillName, $"Disabled overlay without defined damage group on {entity}."); + damageVisComp.Valid = false; + return; + } + } + + if (damageVisComp.DamageOverlayGroups != null && damageVisComp.DamageGroup != null) + { + Logger.WarningS(SawmillName, $"Damage overlay sprites and damage group are both defined on {entity}."); + } + + if (damageVisComp.DamageOverlay != null && damageVisComp.DamageGroup != null) + { + Logger.WarningS(SawmillName, $"Damage overlay sprites and damage group are both defined on {entity}."); + } + } + + private void InitializeVisualizer(EntityUid entity, DamageVisualsComponent damageVisComp) + { + if (!TryComp(entity, out SpriteComponent? spriteComponent) + || !TryComp(entity, out var damageComponent) + || !HasComp(entity)) + return; + + damageVisComp.Thresholds.Add(FixedPoint2.Zero); + damageVisComp.Thresholds.Sort(); + + if (damageVisComp.Thresholds[0] != 0) + { + Logger.ErrorS(SawmillName, $"Thresholds were invalid for entity {entity}. Thresholds: {damageVisComp.Thresholds}"); + damageVisComp.Valid = false; + return; + } + + // If the damage container on our entity's DamageableComponent + // is not null, we can try to check through its groups. + if (damageComponent.DamageContainerID != null + && _prototypeManager.TryIndex(damageComponent.DamageContainerID, out var damageContainer)) + { + // Are we using damage overlay sprites by group? + // Check if the container matches the supported groups, + // and start caching the last threshold. + if (damageVisComp.DamageOverlayGroups != null) + { + foreach (var damageType in damageVisComp.DamageOverlayGroups.Keys) + { + if (!damageContainer.SupportedGroups.Contains(damageType)) + { + Logger.ErrorS(SawmillName, $"Damage key {damageType} was invalid for entity {entity}."); + damageVisComp.Valid = false; + return; + } + + damageVisComp.LastThresholdPerGroup.Add(damageType, FixedPoint2.Zero); + } + } + // Are we tracking a single damage group without overlay instead? + // See if that group is in our entity's damage container. + else if (!damageVisComp.Overlay && damageVisComp.DamageGroup != null) + { + if (!damageContainer.SupportedGroups.Contains(damageVisComp.DamageGroup)) + { + Logger.ErrorS(SawmillName, $"Damage keys were invalid for entity {entity}."); + damageVisComp.Valid = false; + return; + } + + damageVisComp.LastThresholdPerGroup.Add(damageVisComp.DamageGroup, FixedPoint2.Zero); + } + } + // Ditto above, but instead we go through every group. + else // oh boy! time to enumerate through every single group! + { + var damagePrototypeIdList = _prototypeManager.EnumeratePrototypes() + .Select((p, _) => p.ID) + .ToList(); + if (damageVisComp.DamageOverlayGroups != null) + { + foreach (var damageType in damageVisComp.DamageOverlayGroups.Keys) + { + if (!damagePrototypeIdList.Contains(damageType)) + { + Logger.ErrorS(SawmillName, $"Damage keys were invalid for entity {entity}."); + damageVisComp.Valid = false; + return; + } + damageVisComp.LastThresholdPerGroup.Add(damageType, FixedPoint2.Zero); + } + } + else if (damageVisComp.DamageGroup != null) + { + if (!damagePrototypeIdList.Contains(damageVisComp.DamageGroup)) + { + Logger.ErrorS(SawmillName, $"Damage keys were invalid for entity {entity}."); + damageVisComp.Valid = false; + return; + } + + damageVisComp.LastThresholdPerGroup.Add(damageVisComp.DamageGroup, FixedPoint2.Zero); + } + } + + // If we're targeting any layers, and the amount of + // layers is greater than zero, we start reserving + // all the layers needed to track damage groups + // on the entity. + if (damageVisComp.TargetLayers is { Count: > 0 }) + { + // This should ensure that the layers we're targeting + // are valid for the visualizer's use. + // + // If the layer doesn't have a base state, or + // the layer key just doesn't exist, we skip it. + foreach (var key in damageVisComp.TargetLayers) + { + if (!spriteComponent.LayerMapTryGet(key, out var index) + || spriteComponent.LayerGetState(index).ToString() == null) + { + Logger.WarningS(SawmillName, $"Layer at key {key} was invalid for entity {entity}."); + continue; + } + + damageVisComp.TargetLayerMapKeys.Add(key); + } + + // Similar to damage overlay groups, if none of the targeted + // sprite layers could be used, we display an error and + // invalidate the visualizer without crashing. + if (damageVisComp.TargetLayerMapKeys.Count == 0) + { + Logger.ErrorS(SawmillName, $"Target layers were invalid for entity {entity}."); + damageVisComp.Valid = false; + return; + } + + // Otherwise, we start reserving layers. Since the filtering + // loop above ensures that all of these layers are not null, + // and have valid state IDs, there should be no issues. + foreach (object layer in damageVisComp.TargetLayerMapKeys) + { + var layerCount = spriteComponent.AllLayers.Count(); + var index = spriteComponent.LayerMapGet(layer); + var layerState = spriteComponent.LayerGetState(index).ToString()!; + + if (index + 1 != layerCount) + { + index += 1; + } + + damageVisComp.LayerMapKeyStates.Add(layer, layerState); + + // If we're an overlay, and we're targeting groups, + // we reserve layers per damage group. + if (damageVisComp.Overlay && damageVisComp.DamageOverlayGroups != null) + { + foreach (var (group, sprite) in damageVisComp.DamageOverlayGroups) + { + AddDamageLayerToSprite(spriteComponent, + sprite, + $"{layerState}_{group}_{damageVisComp.Thresholds[1]}", + $"{layer}{group}", + index); + } + damageVisComp.DisabledLayers.Add(layer, false); + } + // If we're not targeting groups, and we're still + // using an overlay, we instead just add a general + // overlay that reflects on how much damage + // was taken. + else if (damageVisComp.DamageOverlay != null) + { + AddDamageLayerToSprite(spriteComponent, + damageVisComp.DamageOverlay, + $"{layerState}_{damageVisComp.Thresholds[1]}", + $"{layer}trackDamage", + index); + damageVisComp.DisabledLayers.Add(layer, false); + } + } + } + // If we're not targeting layers, however, + // we should ensure that we instead + // reserve it as an overlay. + else + { + if (damageVisComp.DamageOverlayGroups != null) + { + foreach (var (group, sprite) in damageVisComp.DamageOverlayGroups) + { + AddDamageLayerToSprite(spriteComponent, + sprite, + $"DamageOverlay_{group}_{damageVisComp.Thresholds[1]}", + $"DamageOverlay{group}"); + damageVisComp.TopMostLayerKey = $"DamageOverlay{group}"; + } + } + else if (damageVisComp.DamageOverlay != null) + { + AddDamageLayerToSprite(spriteComponent, + damageVisComp.DamageOverlay, + $"DamageOverlay_{damageVisComp.Thresholds[1]}", + "DamageOverlay"); + damageVisComp.TopMostLayerKey = $"DamageOverlay"; + } + } + } + + /// + /// Adds a damage tracking layer to a given sprite component. + /// + private void AddDamageLayerToSprite(SpriteComponent spriteComponent, DamageVisualizerSprite sprite, string state, string mapKey, int? index = null) + { + var newLayer = spriteComponent.AddLayer( + new SpriteSpecifier.Rsi( + new ResourcePath(sprite.Sprite), state + ), index); + spriteComponent.LayerMapSet(mapKey, newLayer); + if (sprite.Color != null) + spriteComponent.LayerSetColor(newLayer, Color.FromHex(sprite.Color)); + spriteComponent.LayerSetVisible(newLayer, false); + } + + protected override void OnAppearanceChange(EntityUid uid, DamageVisualsComponent damageVisComp, ref AppearanceChangeEvent args) + { + // how is this still here? + if (!damageVisComp.Valid) + return; + + // If this was passed into the component, we update + // the data to ensure that the current disabled + // bool matches. + if (args.Component.TryGetData(DamageVisualizerKeys.Disabled, out var disabledStatus)) + damageVisComp.Disabled = disabledStatus; + + if (damageVisComp.Disabled) + return; + + HandleDamage(args.Component, damageVisComp); + } + + private void HandleDamage(AppearanceComponent component, DamageVisualsComponent damageVisComp) + { + if (!TryComp(component.Owner, out SpriteComponent? spriteComponent) + || !TryComp(component.Owner, out DamageableComponent? damageComponent)) + return; + + if (damageVisComp.TargetLayers != null && damageVisComp.DamageOverlayGroups != null) + UpdateDisabledLayers(spriteComponent, component, damageVisComp); + + if (damageVisComp.Overlay && damageVisComp.DamageOverlayGroups != null && damageVisComp.TargetLayers == null) + CheckOverlayOrdering(spriteComponent, damageVisComp); + + if (component.TryGetData(DamageVisualizerKeys.ForceUpdate, out var update) + && update) + { + ForceUpdateLayers(damageComponent, spriteComponent, damageVisComp); + return; + } + + if (damageVisComp.TrackAllDamage) + { + UpdateDamageVisuals(damageComponent, spriteComponent, damageVisComp); + } + else if (component.TryGetData(DamageVisualizerKeys.DamageUpdateGroups, out DamageVisualizerGroupData data)) + { + UpdateDamageVisuals(data.GroupList, damageComponent, spriteComponent, damageVisComp); + } + } + + /// + /// Checks if any layers were disabled in the last + /// data update. Disabled layers mean that the + /// layer will no longer be visible, or obtain + /// any damage updates. + /// + private void UpdateDisabledLayers(SpriteComponent spriteComponent, AppearanceComponent component, DamageVisualsComponent damageVisComp) + { + foreach (var layer in damageVisComp.TargetLayerMapKeys) + { + bool? layerStatus = null; + if (component.TryGetData(layer, out var layerStateEnum)) + layerStatus = layerStateEnum; + + if (layerStatus == null) + continue; + + if (damageVisComp.DisabledLayers[layer] != (bool) layerStatus) + { + damageVisComp.DisabledLayers[layer] = (bool) layerStatus; + if (!damageVisComp.TrackAllDamage && damageVisComp.DamageOverlayGroups != null) + { + foreach (var damageGroup in damageVisComp.DamageOverlayGroups!.Keys) + { + spriteComponent.LayerSetVisible($"{layer}{damageGroup}", damageVisComp.DisabledLayers[layer]); + } + } + else if (damageVisComp.TrackAllDamage) + spriteComponent.LayerSetVisible($"{layer}trackDamage", damageVisComp.DisabledLayers[layer]); + } + } + } + + /// + /// Checks the overlay ordering on the current + /// sprite component, compared to the + /// data for the visualizer. If the top + /// most layer doesn't match, the sprite + /// layers are recreated and placed on top. + /// + private void CheckOverlayOrdering(SpriteComponent spriteComponent, DamageVisualsComponent damageVisComp) + { + if (spriteComponent[damageVisComp.TopMostLayerKey] != spriteComponent[spriteComponent.AllLayers.Count() - 1]) + { + if (!damageVisComp.TrackAllDamage && damageVisComp.DamageOverlayGroups != null) + { + foreach (var (damageGroup, sprite) in damageVisComp.DamageOverlayGroups) + { + var threshold = damageVisComp.LastThresholdPerGroup[damageGroup]; + ReorderOverlaySprite(spriteComponent, + damageVisComp, + sprite, + $"DamageOverlay{damageGroup}", + $"DamageOverlay_{damageGroup}", + threshold); + } + } + else if (damageVisComp.TrackAllDamage && damageVisComp.DamageOverlay != null) + { + ReorderOverlaySprite(spriteComponent, + damageVisComp, + damageVisComp.DamageOverlay, + $"DamageOverlay", + $"DamageOverlay", + damageVisComp.LastDamageThreshold); + } + } + } + + private void ReorderOverlaySprite(SpriteComponent spriteComponent, DamageVisualsComponent damageVisComp, DamageVisualizerSprite sprite, string key, string statePrefix, FixedPoint2 threshold) + { + spriteComponent.LayerMapTryGet(key, out var spriteLayer); + var visibility = spriteComponent[spriteLayer].Visible; + spriteComponent.RemoveLayer(spriteLayer); + if (threshold == FixedPoint2.Zero) // these should automatically be invisible + threshold = damageVisComp.Thresholds[1]; + spriteLayer = spriteComponent.AddLayer( + new SpriteSpecifier.Rsi( + new ResourcePath(sprite.Sprite), + $"{statePrefix}_{threshold}" + ), + spriteLayer); + spriteComponent.LayerMapSet(key, spriteLayer); + spriteComponent.LayerSetVisible(spriteLayer, visibility); + // this is somewhat iffy since it constantly reallocates + damageVisComp.TopMostLayerKey = key; + } + + /// + /// Updates damage visuals without tracking + /// any damage groups. + /// + private void UpdateDamageVisuals(DamageableComponent damageComponent, SpriteComponent spriteComponent, DamageVisualsComponent damageVisComp) + { + if (!CheckThresholdBoundary(damageComponent.TotalDamage, damageVisComp.LastDamageThreshold, damageVisComp, out var threshold)) + return; + + damageVisComp.LastDamageThreshold = threshold; + + if (damageVisComp.TargetLayers != null) + { + foreach (var layerMapKey in damageVisComp.TargetLayerMapKeys) + { + UpdateTargetLayer(spriteComponent, damageVisComp, layerMapKey, threshold); + } + } + else + { + UpdateOverlay(spriteComponent, threshold); + } + } + + /// + /// Updates damage visuals by damage group, + /// according to the list of damage groups + /// passed into it. + /// + private void UpdateDamageVisuals(List delta, DamageableComponent damageComponent, SpriteComponent spriteComponent, DamageVisualsComponent damageVisComp) + { + foreach (var damageGroup in delta) + { + if (!damageVisComp.Overlay && damageGroup != damageVisComp.DamageGroup) + continue; + + if (!_prototypeManager.TryIndex(damageGroup, out var damageGroupPrototype) + || !damageComponent.Damage.TryGetDamageInGroup(damageGroupPrototype, out var damageTotal)) + continue; + + if (!damageVisComp.LastThresholdPerGroup.TryGetValue(damageGroup, out var lastThreshold) + || !CheckThresholdBoundary(damageTotal, lastThreshold, damageVisComp, out var threshold)) + continue; + + damageVisComp.LastThresholdPerGroup[damageGroup] = threshold; + + if (damageVisComp.TargetLayers != null) + { + foreach (var layerMapKey in damageVisComp.TargetLayerMapKeys) + { + UpdateTargetLayer(spriteComponent, damageVisComp, layerMapKey, damageGroup, threshold); + } + } + else + { + UpdateOverlay(spriteComponent, damageVisComp, damageGroup, threshold); + } + } + + } + + /// + /// Checks if a threshold boundary was passed. + /// + private bool CheckThresholdBoundary(FixedPoint2 damageTotal, FixedPoint2 lastThreshold, DamageVisualsComponent damageVisComp, out FixedPoint2 threshold) + { + threshold = FixedPoint2.Zero; + damageTotal = damageTotal / damageVisComp.Divisor; + var thresholdIndex = damageVisComp.Thresholds.BinarySearch(damageTotal); + + if (thresholdIndex < 0) + { + thresholdIndex = ~thresholdIndex; + threshold = damageVisComp.Thresholds[thresholdIndex - 1]; + } + else + { + threshold = damageVisComp.Thresholds[thresholdIndex]; + } + + if (threshold == lastThreshold) + return false; + + return true; + } + + /// + /// This is the entry point for + /// forcing an update on all damage layers. + /// Does different things depending on + /// the configuration of the visualizer. + /// + private void ForceUpdateLayers(DamageableComponent damageComponent, SpriteComponent spriteComponent, DamageVisualsComponent damageVisComp) + { + if (damageVisComp.DamageOverlayGroups != null) + { + UpdateDamageVisuals(damageVisComp.DamageOverlayGroups.Keys.ToList(), damageComponent, spriteComponent, damageVisComp); + } + else if (damageVisComp.DamageGroup != null) + { + UpdateDamageVisuals(new List(){ damageVisComp.DamageGroup }, damageComponent, spriteComponent, damageVisComp); + } + else if (damageVisComp.DamageOverlay != null) + { + UpdateDamageVisuals(damageComponent, spriteComponent, damageVisComp); + } + } + + /// + /// Updates a target layer. Without a damage group passed in, + /// it assumes you're updating a layer that is tracking all + /// damage. + /// + private void UpdateTargetLayer(SpriteComponent spriteComponent, DamageVisualsComponent damageVisComp, object layerMapKey, FixedPoint2 threshold) + { + if (damageVisComp.Overlay && damageVisComp.DamageOverlayGroups != null) + { + if (!damageVisComp.DisabledLayers[layerMapKey]) + { + var layerState = damageVisComp.LayerMapKeyStates[layerMapKey]; + spriteComponent.LayerMapTryGet($"{layerMapKey}trackDamage", out var spriteLayer); + + UpdateDamageLayerState(spriteComponent, + spriteLayer, + $"{layerState}", + threshold); + } + } + else if (!damageVisComp.Overlay) + { + var layerState = damageVisComp.LayerMapKeyStates[layerMapKey]; + spriteComponent.LayerMapTryGet(layerMapKey, out var spriteLayer); + + UpdateDamageLayerState(spriteComponent, + spriteLayer, + $"{layerState}", + threshold); + } + } + + /// + /// Updates a target layer by damage group. + /// + private void UpdateTargetLayer(SpriteComponent spriteComponent, DamageVisualsComponent damageVisComp, object layerMapKey, string damageGroup, FixedPoint2 threshold) + { + if (damageVisComp.Overlay && damageVisComp.DamageOverlayGroups != null) + { + if (damageVisComp.DamageOverlayGroups.ContainsKey(damageGroup) && !damageVisComp.DisabledLayers[layerMapKey]) + { + var layerState = damageVisComp.LayerMapKeyStates[layerMapKey]; + spriteComponent.LayerMapTryGet($"{layerMapKey}{damageGroup}", out var spriteLayer); + + UpdateDamageLayerState(spriteComponent, + spriteLayer, + $"{layerState}_{damageGroup}", + threshold); + } + } + else if (!damageVisComp.Overlay) + { + var layerState = damageVisComp.LayerMapKeyStates[layerMapKey]; + spriteComponent.LayerMapTryGet(layerMapKey, out var spriteLayer); + + UpdateDamageLayerState(spriteComponent, + spriteLayer, + $"{layerState}_{damageGroup}", + threshold); + } + } + + /// + /// Updates an overlay that is tracking all damage. + /// + private void UpdateOverlay(SpriteComponent spriteComponent, FixedPoint2 threshold) + { + spriteComponent.LayerMapTryGet($"DamageOverlay", out var spriteLayer); + + UpdateDamageLayerState(spriteComponent, + spriteLayer, + $"DamageOverlay", + threshold); + } + + /// + /// Updates an overlay based on damage group. + /// + private void UpdateOverlay(SpriteComponent spriteComponent, DamageVisualsComponent damageVisComp, string damageGroup, FixedPoint2 threshold) + { + if (damageVisComp.DamageOverlayGroups != null) + { + if (damageVisComp.DamageOverlayGroups.ContainsKey(damageGroup)) + { + spriteComponent.LayerMapTryGet($"DamageOverlay{damageGroup}", out var spriteLayer); + + UpdateDamageLayerState(spriteComponent, + spriteLayer, + $"DamageOverlay_{damageGroup}", + threshold); + } + } + } + + /// + /// Updates a layer on the sprite by what + /// prefix it has (calculated by whatever + /// function calls it), and what threshold + /// was passed into it. + /// + private void UpdateDamageLayerState(SpriteComponent spriteComponent, int spriteLayer, string statePrefix, FixedPoint2 threshold) + { + if (threshold == 0) + { + spriteComponent.LayerSetVisible(spriteLayer, false); + } + else + { + if (!spriteComponent[spriteLayer].Visible) + { + spriteComponent.LayerSetVisible(spriteLayer, true); + } + spriteComponent.LayerSetState(spriteLayer, $"{statePrefix}_{threshold}"); + } + } +} diff --git a/Content.Client/Revenant/Ui/RevenantBoundUserInterface.cs b/Content.Client/Revenant/Ui/RevenantBoundUserInterface.cs deleted file mode 100644 index 17f7d88875c86a..00000000000000 --- a/Content.Client/Revenant/Ui/RevenantBoundUserInterface.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Content.Shared.Revenant; -using JetBrains.Annotations; -using Robust.Client.GameObjects; - -namespace Content.Client.Revenant.Ui; - -[UsedImplicitly] -public sealed class RevenantBoundUserInterface : BoundUserInterface -{ - private RevenantMenu? _menu; - - public RevenantBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) - { - - } - - protected override void Open() - { - _menu = new(); - _menu.OpenCentered(); - _menu.OnClose += Close; - - _menu.OnListingButtonPressed += (_, listing) => - { - SendMessage(new RevenantBuyListingMessage(listing)); - }; - } - - protected override void UpdateState(BoundUserInterfaceState state) - { - base.UpdateState(state); - - if (_menu == null) - return; - - switch (state) - { - case RevenantUpdateState msg: - _menu.UpdateEssence(msg.Essence); - _menu.UpdateListing(msg.Listings); - break; - } - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposing) - return; - - _menu?.Close(); - _menu?.Dispose(); - } -} diff --git a/Content.Client/Revenant/Ui/RevenantListingControl.xaml b/Content.Client/Revenant/Ui/RevenantListingControl.xaml deleted file mode 100644 index 6adcfdc8965111..00000000000000 --- a/Content.Client/Revenant/Ui/RevenantListingControl.xaml +++ /dev/null @@ -1,21 +0,0 @@ - - - -