Files
tbd-station-14/Content.Shared/Damage/DamageSpecifier.cs
Darkie a3fbab84e6 ItemToggle system expansion (#22369)
* Fixed EnergySword and variants having incorrect sound on attacking when in their Off state.

* Removed the unused ItemToggle from the serverside and created a new shared ItemToggleComponent and System, now used for the e-blade family of items. Also added e-blade hum and swing sounds. Thanks Sloth for the initial code!

* Changing Stunbaton system to include the itemToggle system.

* Adapted changes that have come up in the meantime.

* Changed damagespecifier to be serializable and autoNetworked in melee weapon components. Fixes a bug that makes it so client-side, damage values are not updated on toggle.

* Made the ItemToggleSystem have both a shared and a server component. Ported the Stun Baton and Stun Prod to the new toggleable system. Added a failure to activate noise component.

* Ported the welders to the new item toggle system. Set it so deactivated damage and item size default to the item's regular options.

* Removed unnecessary usings.

* Small modification to the stun prod.

* Made the integration test use the new method to turn the welders on.

* Fixed a few testing issues, applied a few changes requested by Delta.

* Updated Stunbaton code for consistentcy when it comes to calling the itemToggle component.

* Removed a redundant return; as per Delta.
Made examining the stun baton for charge rely on the battery component instead.

* Removed the welder visualizer system, now using the generic one. Removed some unused usings. Removed the welder visuals and layers.
Ported lighters to the new system.
Added zippi (sic) lighters.

* Renamed variables used to make them less generic.

* Simplified the light update code.

* Fixed the unit test to use the itemToggle system for welders now.

* Made the name shorter. I can't tell if the welding damage when interacted with actually does anything though. I can't figure out how to trigger it.

* Fixed some YML issues.

* Added a client side item toggle system just to make the shared code run on local UID's too.

* Fixed some more Yaml.

* Made the Zippi lighter have its own parent item, so it doesnt' conflict with the random pattern on the regular lighter.

* Made the zippi lighter its own in-hand sprites.

* Added a summary for the activated property in itemtoggle component.

* Fixed a typo in the itemToggle Component.

* Fixed a typo.

* Added to the remarks for the ItemToggleComponent.

* Fixed up the lighter yaml to make it use a generic term instead of a toggle layer enum for the random skin.

* Fixed a bug I introduced accidentally with the humming sound.

* Removed 2 unnecessary events from the ItemToggleSystem and component.

* Fixed a bug by only making the server run the item activation code, since the client cannot predict whether or not the activation will be cancelled.

* Cleaned up some names and functions getting called.

* Renamed a couple of variables and removed the explicit datafields from the component. Removed "activated: false" from yml since they're already deactivated by default.

* Added an IsActivated function, used it in the welder and stun baton systems code.
Refactored welder code to remove the WelderToggle event, now using the ItemToggleActivatedEvent instead for eye protection check.

* Fixed a typo. Added some comments.

* Split the ItemToggle into smaller components.
Changed the items that used the toggle system to work with the smaller components.
Made the mirror shield reflect energy shots with a 95% chance.

* Fixed the namespaces for the server components and whatnot.

* Fixed a doubled deactivation sound from using activated wieldable items (like the double Esword).
Fixed wrong yml with the e-dagger.
Fixed the disarm malus code.

* Added the zippo lighter to the detective's trench coat.

* Removed the default hit sound for the double e-sword since it was unnecessary.

* Changed e-sword damage numbers to be in line with the changes made by Emisse.

* Made no damage sounds be autoNetworked, so it changes can be changed on activation/deactivation of items.
Made Welders and Eswords sound like themselves but quieter if they hit for 0 damage, instead of taps.
You can choose what sound to play when a weapon does 0 damage when activated now.
Fixed a bug with swing sounds.

* Typo.

* Fixed a bug where the welder would blind you if you used it while it was off.

* Created a single abstract method called when an item has completed its toggle.

* Update Content.Server/Eye/Blinding/EyeProtection/EyeProtectionSystem.cs

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>

* Fixed a comment.

* Made most component variables readOnly for ItemToggle. There is no need to be able to change them from within the variable viewer.

* Removed trailing white spaces.

* Made the Use a field instead of a property in the itemToggleActivation/Deactivation attempt events.

* Small fixes.

* Removed ForceToggle, just use the toggle method instead.

* Fixed a bug with item sharpness staying even after getting deactivated, if the item gained sharpness that way (esword).

* Used ProtoId in the welder component.

* Made damage NetSerializable as well.

* Added networking and data fields to a couple of components.

* Made component variables autonetworked. Added some comments.

* Moved the events that modify item components on toggle to events, handled (where possible) in the systems linked to said components.

* Made all the component variables readWrite again.

* Added the component get to the WelderStatus.

* Added a predictable bool to the item toggle component.

* Replaced the Activated/Deactivated events with ToggleDone, with an Activated argument. Used that to simplify some systems.

* Added a reflect update raise event.

* Removed the Zippo changes. To add in a later PR.

* Removed the zippo from meta.json too.

* Small fix.

* Another small fix.

* Fixed the wieldable system thing in ItemToggle.

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2023-12-24 17:11:05 +11:00

416 lines
16 KiB
C#

using System.Text.Json.Serialization;
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
using Robust.Shared.Utility;
using Robust.Shared.Serialization;
namespace Content.Shared.Damage
{
/// <summary>
/// This class represents a collection of damage types and damage values.
/// </summary>
/// <remarks>
/// The actual damage information is stored in <see cref="DamageDict"/>. This class provides
/// functions to apply resistance sets and supports basic math operations to modify this dictionary.
/// </remarks>
[DataDefinition, Serializable, NetSerializable]
public sealed partial class DamageSpecifier : IEquatable<DamageSpecifier>
{
// These exist solely so the wiki works. Please do not touch them or use them.
[JsonPropertyName("types")]
[DataField("types", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<FixedPoint2, DamageTypePrototype>))]
[UsedImplicitly]
private Dictionary<string,FixedPoint2>? _damageTypeDictionary;
[JsonPropertyName("groups")]
[DataField("groups", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<FixedPoint2, DamageGroupPrototype>))]
[UsedImplicitly]
private Dictionary<string, FixedPoint2>? _damageGroupDictionary;
/// <summary>
/// Main DamageSpecifier dictionary. Most DamageSpecifier functions exist to somehow modifying this.
/// </summary>
[JsonIgnore]
[ViewVariables(VVAccess.ReadWrite)]
[IncludeDataField(customTypeSerializer: typeof(DamageSpecifierDictionarySerializer), readOnly: true)]
public Dictionary<string, FixedPoint2> DamageDict { get; set; } = new();
[JsonIgnore]
[Obsolete("Use GetTotal()")]
public FixedPoint2 Total => GetTotal();
/// <summary>
/// Returns a sum of the damage values.
/// </summary>
/// <remarks>
/// Note that this being zero does not mean this damage has no effect. Healing in one type may cancel damage
/// in another. Consider using <see cref="Any()"/> or <see cref="Empty"/> instead.
/// </remarks>
public FixedPoint2 GetTotal()
{
var total = FixedPoint2.Zero;
foreach (var value in DamageDict.Values)
{
total += value;
}
return total;
}
/// <summary>
/// Returns true if the specifier contains any positive damage values.
/// Differs from <see cref="Empty"/> as a damage specifier might contain entries with zeroes.
/// This also returns false if the specifier only contains negative values.
/// </summary>
public bool Any()
{
foreach (var value in DamageDict.Values)
{
if (value > FixedPoint2.Zero)
return true;
}
return false;
}
/// <summary>
/// Whether this damage specifier has any entries.
/// </summary>
[JsonIgnore]
public bool Empty => DamageDict.Count == 0;
#region constructors
/// <summary>
/// Constructor that just results in an empty dictionary.
/// </summary>
public DamageSpecifier() { }
/// <summary>
/// Constructor that takes another DamageSpecifier instance and copies it.
/// </summary>
public DamageSpecifier(DamageSpecifier damageSpec)
{
DamageDict = new(damageSpec.DamageDict);
}
/// <summary>
/// Constructor that takes a single damage type prototype and a damage value.
/// </summary>
public DamageSpecifier(DamageTypePrototype type, FixedPoint2 value)
{
DamageDict = new() { { type.ID, value } };
}
/// <summary>
/// Constructor that takes a single damage group prototype and a damage value. The value is divided between members of the damage group.
/// </summary>
public DamageSpecifier(DamageGroupPrototype group, FixedPoint2 value)
{
// Simply distribute evenly (except for rounding).
// We do this by reducing remaining the # of types and damage every loop.
var remainingTypes = group.DamageTypes.Count;
var remainingDamage = value;
foreach (var damageType in group.DamageTypes)
{
var damage = remainingDamage / FixedPoint2.New(remainingTypes);
DamageDict.Add(damageType, damage);
remainingDamage -= damage;
remainingTypes -= 1;
}
}
#endregion constructors
/// <summary>
/// Reduce (or increase) damages by applying a damage modifier set.
/// </summary>
/// <remarks>
/// Only applies resistance to a damage type if it is dealing damage, not healing.
/// This will never convert damage into healing.
/// </remarks>
public static DamageSpecifier ApplyModifierSet(DamageSpecifier damageSpec, DamageModifierSet modifierSet)
{
// Make a copy of the given data. Don't modify the one passed to this function. I did this before, and weapons became
// duller as you hit walls. Neat, but not FixedPoint2ended. And confusing, when you realize your fists don't work no
// more cause they're just bloody stumps.
DamageSpecifier newDamage = new();
newDamage.DamageDict.EnsureCapacity(damageSpec.DamageDict.Count);
foreach (var (key, value) in damageSpec.DamageDict)
{
if (value == 0)
continue;
if (value < 0)
{
newDamage.DamageDict[key] = value;
continue;
}
float newValue = value.Float();
if (modifierSet.FlatReduction.TryGetValue(key, out var reduction))
newValue -= reduction;
if (modifierSet.Coefficients.TryGetValue(key, out var coefficient))
newValue *= coefficient;
if (newValue > 0)
newDamage.DamageDict[key] = FixedPoint2.New(newValue);
}
return newDamage;
}
/// <summary>
/// Reduce (or increase) damages by applying multiple modifier sets.
/// </summary>
/// <param name="damageSpec"></param>
/// <param name="modifierSets"></param>
/// <returns></returns>
public static DamageSpecifier ApplyModifierSets(DamageSpecifier damageSpec, IEnumerable<DamageModifierSet> modifierSets)
{
bool any = false;
DamageSpecifier newDamage = damageSpec;
foreach (var set in modifierSets)
{
// This creates a new damageSpec for each modifier when we really onlt need to create one.
// This is quite inefficient, but hopefully this shouldn't ever be called frequently.
newDamage = ApplyModifierSet(newDamage, set);
any = true;
}
if (!any)
newDamage = new DamageSpecifier(damageSpec);
return newDamage;
}
/// <summary>
/// Remove any damage entries with zero damage.
/// </summary>
public void TrimZeros()
{
foreach (var (key, value) in DamageDict)
{
if (value == 0)
{
DamageDict.Remove(key);
}
}
}
/// <summary>
/// Clamps each damage value to be within the given range.
/// </summary>
public void Clamp(FixedPoint2 minValue, FixedPoint2 maxValue)
{
DebugTools.Assert(minValue < maxValue);
ClampMax(maxValue);
ClampMin(minValue);
}
/// <summary>
/// Sets all damage values to be at least as large as the given number.
/// </summary>
/// <remarks>
/// Note that this only acts on damage types present in the dictionary. It will not add new damage types.
/// </remarks>
public void ClampMin(FixedPoint2 minValue)
{
foreach (var (key, value) in DamageDict)
{
if (value < minValue)
{
DamageDict[key] = minValue;
}
}
}
/// <summary>
/// Sets all damage values to be at most some number. Note that if a damage type is not present in the
/// dictionary, these will not be added.
/// </summary>
public void ClampMax(FixedPoint2 maxValue)
{
foreach (var (key, value) in DamageDict)
{
if (value > maxValue)
{
DamageDict[key] = maxValue;
}
}
}
/// <summary>
/// This adds the damage values of some other <see cref="DamageSpecifier"/> to the current one without
/// adding any new damage types.
/// </summary>
/// <remarks>
/// This is used for <see cref="DamageableComponent"/>s, such that only "supported" damage types are
/// actually added to the component. In most other instances, you can just use the addition operator.
/// </remarks>
public void ExclusiveAdd(DamageSpecifier other)
{
foreach (var (type, value) in other.DamageDict)
{
// CollectionsMarshal my beloved.
if (DamageDict.TryGetValue(type, out var existing))
{
DamageDict[type] = existing + value;
}
}
}
/// <summary>
/// Add up all the damage values for damage types that are members of a given group.
/// </summary>
/// <remarks>
/// If no members of the group are included in this specifier, returns false.
/// </remarks>
public bool TryGetDamageInGroup(DamageGroupPrototype group, out FixedPoint2 total)
{
bool containsMemeber = false;
total = FixedPoint2.Zero;
foreach (var type in group.DamageTypes)
{
if (DamageDict.TryGetValue(type, out var value))
{
total += value;
containsMemeber = true;
}
}
return containsMemeber;
}
/// <summary>
/// Returns a dictionary using <see cref="DamageGroupPrototype.ID"/> keys, with values calculated by adding
/// up the values for each damage type in that group
/// </summary>
/// <remarks>
/// If a damage type is associated with more than one supported damage group, it will contribute to the
/// total of each group. If no members of a group are present in this <see cref="DamageSpecifier"/>, the
/// group is not included in the resulting dictionary.
/// </remarks>
public Dictionary<string, FixedPoint2> GetDamagePerGroup(IPrototypeManager protoManager)
{
var dict = new Dictionary<string, FixedPoint2>();
GetDamagePerGroup(protoManager, dict);
return dict;
}
/// <inheritdoc cref="GetDamagePerGroup(Robust.Shared.Prototypes.IPrototypeManager)"/>
public void GetDamagePerGroup(IPrototypeManager protoManager, Dictionary<string, FixedPoint2> dict)
{
dict.Clear();
foreach (var group in protoManager.EnumeratePrototypes<DamageGroupPrototype>())
{
if (TryGetDamageInGroup(group, out var value))
dict.Add(group.ID, value);
}
}
#region Operators
public static DamageSpecifier operator *(DamageSpecifier damageSpec, FixedPoint2 factor)
{
DamageSpecifier newDamage = new();
foreach (var entry in damageSpec.DamageDict)
{
newDamage.DamageDict.Add(entry.Key, entry.Value * factor);
}
return newDamage;
}
public static DamageSpecifier operator *(DamageSpecifier damageSpec, float factor)
{
DamageSpecifier newDamage = new();
foreach (var entry in damageSpec.DamageDict)
{
newDamage.DamageDict.Add(entry.Key, entry.Value * factor);
}
return newDamage;
}
public static DamageSpecifier operator /(DamageSpecifier damageSpec, FixedPoint2 factor)
{
DamageSpecifier newDamage = new();
foreach (var entry in damageSpec.DamageDict)
{
newDamage.DamageDict.Add(entry.Key, entry.Value / factor);
}
return newDamage;
}
public static DamageSpecifier operator /(DamageSpecifier damageSpec, float factor)
{
DamageSpecifier newDamage = new();
foreach (var entry in damageSpec.DamageDict)
{
newDamage.DamageDict.Add(entry.Key, entry.Value / factor);
}
return newDamage;
}
public static DamageSpecifier operator +(DamageSpecifier damageSpecA, DamageSpecifier damageSpecB)
{
// Copy existing dictionary from dataA
DamageSpecifier newDamage = new(damageSpecA);
// Then just add types in B
foreach (var entry in damageSpecB.DamageDict)
{
if (!newDamage.DamageDict.TryAdd(entry.Key, entry.Value))
{
// Key already exists, add values
newDamage.DamageDict[entry.Key] += entry.Value;
}
}
return newDamage;
}
// Here we define the subtraction operator explicitly, rather than implicitly via something like X + (-1 * Y).
// This is faster because FixedPoint2 multiplication is somewhat involved.
public static DamageSpecifier operator -(DamageSpecifier damageSpecA, DamageSpecifier damageSpecB)
{
DamageSpecifier newDamage = new(damageSpecA);
foreach (var entry in damageSpecB.DamageDict)
{
if (!newDamage.DamageDict.TryAdd(entry.Key, -entry.Value))
{
newDamage.DamageDict[entry.Key] -= entry.Value;
}
}
return newDamage;
}
public static DamageSpecifier operator +(DamageSpecifier damageSpec) => damageSpec;
public static DamageSpecifier operator -(DamageSpecifier damageSpec) => damageSpec * -1;
public static DamageSpecifier operator *(float factor, DamageSpecifier damageSpec) => damageSpec * factor;
public static DamageSpecifier operator *(FixedPoint2 factor, DamageSpecifier damageSpec) => damageSpec * factor;
public bool Equals(DamageSpecifier? other)
{
if (other == null || DamageDict.Count != other.DamageDict.Count)
return false;
foreach (var (key, value) in DamageDict)
{
if (!other.DamageDict.TryGetValue(key, out var otherValue) || value != otherValue)
return false;
}
return true;
}
public FixedPoint2 this[string key] => DamageDict[key];
}
#endregion
}