Files
tbd-station-14/Content.Client/Damage/DamageVisualsSystem.cs
Flipp Syder 5a0a04bde7 Humanoid appearance refactor (#10882)
* initial commit
- species prototype modifications
- markings points as its own file
- shared humanoid component

* adds a tool to convert sprite accessories to markings (in go)

* removes a fmt call

* converts sprite accessory to markings

* adds hair and facial hair to marking categories

* multiple changes
- humanoid visualizer system
- markings modifications for visualizer
- modifications to shared humanoid component
- lays out a base for humanoid system

* hidden layers, ports some properties from appearance component, shrinks DefaultMarkings a little

* squishes the initialize event calls into one function

adds stuff to set species/skin color externally from a server message - currently laid out as if it a dirty call to a networked component, may be subject to change (server-side has not been implemented yet)

* makes the sprite pipeline more obvious

* apply all markings, hidden layer set replacement

* ensures that markings are cleared when the new set is applied

* starts refactoring markingsset (unfinished)

* more additions to the markingset api

* adds constructor logic to markingset

* adds a method to filter out markings in a set based on a given species

* fixes enumerators in markingset

* adds validator into MarkingSet, fixes ForwardMarkingEnumerator

* modifications to the humanoid visual system

* ensuredefault in markingset

* oop

* fixes up data keys, populates OnAppearanceChange in visualizer

* changes to humanoid component, markings

marking equality is now more strict, humanoidcomponent is now implemented for client as a child of sharedhumanoidcomponent

* markings are now applied the visualizer by diffing them

* base sprites are now applied to humanoids from humanoidvisualizer

* passes along base sprite settings to the marking application so that markings know to follow skin color/alpha or not (see: slimes)

* custom base layers on humanoids

* merges all data keys into one data class for humanoid visualizers

* setappearance in sharedhumanoidsystem, removes custombaselayercolors

* humanoidcomponent, system (empty) in server

* adds some basic public API functions to HumanoidSystem

* add marking, remove marking

* changes appearance MarkingsSet to a List<Marking>, adds listener for PlayerSpawnCompleteEvent in HumanoidSystem

* ensuredefaultmarkings, oninit for humanoids

* markingmanager API changes

* removes MarkingsSet

* LoadProfile, adjusts randomization in humanoid appearance to account for species

* base layer settings in humanoidsystem, eye color from profile

* rearranges files to centralize under Humanoid namespace

* more reorganization, deletes some stuff

gotta break stuff to make other things work, right?

goodbye SpriteAccessory...

* fixes a good chunk of server-side issues

still does not compile, yet

* singlemarkingpicker xaml layout

* singlemarkingpicker logic

* magic mirror window (varying pieces of it, mostly client-oriented)

* removes some imports, gives MagicMirror a BUI class (not filled in yet)

* populates magic mirror BUI functionality / window callbacks

* fixes up some errors in humanoidprofileeditor

* changes to SingleMarkingPicker

SingleMarkingPicker now accepts a List<Marking>, species, and total possible markings available in that marking category

* fixes up hair pickers on humanoid profile editor

* fixes the errors in markingpicker

* markingsystem is now gone

* fixes a bunch of build errors

* so that's why i did it like that

* namespace issues, adds robustxamlloader to singlemarkingpicker

* another robustxamlloader

* human, lizard sprites/points

* prototype fixes, deletion of old spriteaccessory

* component registration, fixes dwarf skin toning

no, 'ReptilianToned' does not exist

* removes component registration from abstract humanoid component

* visualizer data now cloneable

* serialize for visualizer key

* zero-count edge case

* missing semi-colon moment

* setspecies in humanoidsystem

* ensures that default markings, if empty, will cause ensuredefault to skip over that given category

* tryadd instead of add

* whoops

* diff and apply should properly apply markings now

* always ensure default, fixes double load for player spawning

* apply skin color now sets the skin color property in humanoidcomponent

* removes sprite from a few species prototypes

* sprite changes for specific base layers based on humanoid sex

* layer ordering fix, and a missing base layer should now disallow markings on that layer

* anymarking base layer, adds the right leg/foot for humans

* loading a profile will now clear all markings on that humanoid

* adds missing layers for humans

* separates species.yml into respective species prototype files

* ensures that if layer visibility was changed, all markings have to be reapplied

* server-side enforcement of hiding hair (and other head-related markings) when equipping things that hide hair

* slime fix, clothingsystem now dictates layer visibility server side

* sussy

* layer settings should now ensure a marking should match the skin tone

* whoops

* skincolor static class and functions in UI

* skin color validation in humanoidcharacterappearance

* markingpicker now shows only the markings for the selected category in used

* getter for slot in singlemarkingpicker now ensures slot is 0 if markings exists

* FilterSpecies no longer attempts to do removal while iterating

* expands for SingleMarkingPicker

* humanoid base dummy has blank layers now (and snout/tail/headside/headtop)

* fixes an issue with visualizer system if the marking count was different but the markings themselves were (somewhat) the same

* whoops

* adds edge case handlers for count differences in humanoid markings

* preview now loads profile instead of directly setting appearance

* moves marking set loading to update controls

* clones a marking set in markingpicker by using the deep clone constructor

* whoops (deep cloning a marking now copies the marking id)

* adds replace function for markingset

* points should now update after the markings are remove/added

* merging base layer sprites into a humanoid should now clear them before merging

* sets dirty range start to count only if the dirty range start was never set above 0

* fixes up some issues with singlemarkingpicker

* color selector sliders in single marking picker should now expand

* hair from hair pickers should now apply in profile loading (client-side)

* category in singlemarkingpicker now sets the private category variable

* slot selector should now populate

* single marking picker buttons now have text, also shows the category name over all user-clickable elements

* removes a comment

* removing hair slots now sets it to bald, defaults to zero used slots if current hair is bald on hair/facial hair

* random skin color, eye color

* populate colors now checks if the marking count is greater than zero in singlemarkingpicker

* hair/facial hair pickers now just get the first possible hair from the respective species list

* different approach to random skin color

* oh, that's why it wasn't working

* randomize everything now just updates every single control

* selecting a new marking in SingleMarkingPicker should attempt to copy over old colors, populate list now uses cache,

* markingmanager now uses OnlyWhitelisted to populate by category and species

* filterspecies now uses onlyWhitelist to filter markings based on whitelist or not

* oops

* ui fix for singlemarkingpicker, ensures that cache is not null if it is null when populatelist is called

* order of operations for the horizontal expand for add/remove

* hair pickers should now update when you add/remove the hair slot

* fixes variable naming error in character appearance

* loc string fix in singlemarkingpicker

* lizards, vox now have onlyWhitelist, vox restriction for hair/facialhairs

* having zero possible hairs should no longer cause an exception in randomization

* setting species should now update hair pickers

* ignore categories for marking picker

* and a clear as well for the category button

* places that functionality in its own function instead

* adds eye base sprite, vox now also have their own custom eye sprites

* loading a profile client-side should do FilterSpecies for markings now

* client-side load profile does filter species after adding in the hairs now

* magic mirror

* callbacks now call the callback instead of adding it on construct

* whoops

* in removemarking too

* adds missing synchronize calls

* comments out an updateinterface call in magic mirror

* magic mirror window title, minimum sizing

* fixes minsize, adds warning for players who try to set their hair for species that have no hair

* removes spaces in xaml

* namespace changes/organization

* whoopsie (merge conflicts)

* re-enables identity from humanoid component

* damagevisuals now uses the enum given to it instead of the layerstate given on that layer tied to the enum

* removes commas from json

* changes to visuals system so the change is consistent

* chest

* reptilian

* visualizer system now handles body sprite setting/coloration, similar to how characterappearance did it

not a big fan of this

* adds a check in applybasesprites

* adding/removing parts should now make them invisible on a humanoid

* body part removal/adding now enumerates over sublayers instead

* synchro now runs in bodycomponent startup

* parts instead of slots

* humanoidcompnent check

* switches from rsi to actualrsi

* removes all the body stuff (too slow)

* cleans up resolves from humanoid visualizer system

* merging sprites now checks if the base sprites have been modified or not (through things like species changes, or custom base sprite changes)

* not forgetting that one again

* merging now returns an actual dirty value

* replaces the sequenceequal with a more accurate solution

* permanent layers, layer visibility on add/remove part in body

* should send all hidden layers over now

* isdirty in visualizer system for base layers

* isdirty checks count as well

* ok, IsDirty should now set the base layers if the merged sprites are different

* equals override in HumanoidSpritePrototypes.cs

temporary until record prototypes :heck:

* makes fields readonly, equates IDs instead

* adds forced markings through marking picker

* forced in humanoidsystem api, ignorespecies in markingpicker

* marking bui

* makes that serializable as well

* ignore species/forced toggles now work

* adds icon to modifier verb, interface and keys to humanoid bases

* needs the actual enum value to open, no?

* makes the key the actual key

* actions now propagate upwards

* ignore species when set now repopulates markingpicker

* modifiable base layers in the markings window

* oops!

* layout changes

* info box should now appear

* adds ignorespecies for marking picker, collapsible for base layer section of appearance modification window

* collapsible layout moment

* if base layers have changed, all markings are now dirty (and if a base layer is missing, the marking is still 'applied' but it's now just invisible

* small change to marking visibility

* small changes to modifier UI

* markings now match skin on zombification

* zombie stuff

* makes the line edit in marking modifier window more obvious

* disables vox on round start

* horizontal expand on the single label in base layer modifiers

* humanoid profiles in prototypes

* randomhumanoidappearance won't work if the humanoid has a profile already stored

* removes unused code

* documentation in humanoidsystem server-side

* documentation in shared/client

* whoops

* converts accessory into marking in locale files (also adds marking loc string into single marking picker)

* be gone, shared humanoid appearance system from the last upstream merge

* species ignore on randomization (defaults to no ignored species)

* more upstream merge parts that bypassed any errors before merge

* addresses review (also just adds typeserializers in some places)

* submodule moment

* upstream merge issues
2022-09-22 17:19:00 -05:00

698 lines
28 KiB
C#

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;
/// <summary>
/// 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).
/// </summary>
public sealed class DamageVisualsSystem : VisualizerSystem<DamageVisualsComponent>
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private const string SawmillName = "DamageVisuals";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DamageVisualsComponent, ComponentInit>(InitializeEntity);
}
private void InitializeEntity(EntityUid entity, DamageVisualsComponent comp, ComponentInit args)
{
VerifyVisualizerSetup(entity, comp);
if (!comp.Valid)
{
RemCompDeferred<DamageVisualsComponent>(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<DamageableComponent?>(entity, out var damageComponent)
|| !HasComp<AppearanceComponent>(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<DamageContainerPrototype>(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<DamageGroupPrototype>()
.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))
{
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 (var 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, layer.ToString());
// 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,
$"{layer}_{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,
$"{layer}_{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";
}
}
}
/// <summary>
/// Adds a damage tracking layer to a given sprite component.
/// </summary>
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<bool>(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<bool>(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);
}
}
/// <summary>
/// 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.
/// </summary>
private void UpdateDisabledLayers(SpriteComponent spriteComponent, AppearanceComponent component, DamageVisualsComponent damageVisComp)
{
foreach (var layer in damageVisComp.TargetLayerMapKeys)
{
bool? layerStatus = null;
if (component.TryGetData<bool>(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]);
}
}
}
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary>
/// Updates damage visuals without tracking
/// any damage groups.
/// </summary>
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);
}
}
/// <summary>
/// Updates damage visuals by damage group,
/// according to the list of damage groups
/// passed into it.
/// </summary>
private void UpdateDamageVisuals(List<string> delta, DamageableComponent damageComponent, SpriteComponent spriteComponent, DamageVisualsComponent damageVisComp)
{
foreach (var damageGroup in delta)
{
if (!damageVisComp.Overlay && damageGroup != damageVisComp.DamageGroup)
continue;
if (!_prototypeManager.TryIndex<DamageGroupPrototype>(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);
}
}
}
/// <summary>
/// Checks if a threshold boundary was passed.
/// </summary>
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;
}
/// <summary>
/// This is the entry point for
/// forcing an update on all damage layers.
/// Does different things depending on
/// the configuration of the visualizer.
/// </summary>
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<string>(){ damageVisComp.DamageGroup }, damageComponent, spriteComponent, damageVisComp);
}
else if (damageVisComp.DamageOverlay != null)
{
UpdateDamageVisuals(damageComponent, spriteComponent, damageVisComp);
}
}
/// <summary>
/// Updates a target layer. Without a damage group passed in,
/// it assumes you're updating a layer that is tracking all
/// damage.
/// </summary>
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);
}
}
/// <summary>
/// Updates a target layer by damage group.
/// </summary>
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);
}
}
/// <summary>
/// Updates an overlay that is tracking all damage.
/// </summary>
private void UpdateOverlay(SpriteComponent spriteComponent, FixedPoint2 threshold)
{
spriteComponent.LayerMapTryGet($"DamageOverlay", out var spriteLayer);
UpdateDamageLayerState(spriteComponent,
spriteLayer,
$"DamageOverlay",
threshold);
}
/// <summary>
/// Updates an overlay based on damage group.
/// </summary>
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);
}
}
}
/// <summary>
/// Updates a layer on the sprite by what
/// prefix it has (calculated by whatever
/// function calls it), and what threshold
/// was passed into it.
/// </summary>
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}");
}
}
}