DamageSpecifier.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  1. using System.Text.Json.Serialization;
  2. using Content.Shared.Damage.Prototypes;
  3. using Content.Shared.FixedPoint;
  4. using JetBrains.Annotations;
  5. using Robust.Shared.Prototypes;
  6. using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
  7. using Robust.Shared.Utility;
  8. using Robust.Shared.Serialization;
  9. namespace Content.Shared.Damage
  10. {
  11. /// <summary>
  12. /// This class represents a collection of damage types and damage values.
  13. /// </summary>
  14. /// <remarks>
  15. /// The actual damage information is stored in <see cref="DamageDict"/>. This class provides
  16. /// functions to apply resistance sets and supports basic math operations to modify this dictionary.
  17. /// </remarks>
  18. [DataDefinition, Serializable, NetSerializable]
  19. public sealed partial class DamageSpecifier : IEquatable<DamageSpecifier>
  20. {
  21. // These exist solely so the wiki works. Please do not touch them or use them.
  22. [JsonPropertyName("types")]
  23. [DataField("types", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<FixedPoint2, DamageTypePrototype>))]
  24. [UsedImplicitly]
  25. private Dictionary<string,FixedPoint2>? _damageTypeDictionary;
  26. [JsonPropertyName("groups")]
  27. [DataField("groups", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<FixedPoint2, DamageGroupPrototype>))]
  28. [UsedImplicitly]
  29. private Dictionary<string, FixedPoint2>? _damageGroupDictionary;
  30. /// <summary>
  31. /// Main DamageSpecifier dictionary. Most DamageSpecifier functions exist to somehow modifying this.
  32. /// </summary>
  33. [JsonIgnore]
  34. [ViewVariables(VVAccess.ReadWrite)]
  35. [IncludeDataField(customTypeSerializer: typeof(DamageSpecifierDictionarySerializer), readOnly: true)]
  36. public Dictionary<string, FixedPoint2> DamageDict { get; set; } = new();
  37. /// <summary>
  38. /// Returns a sum of the damage values.
  39. /// </summary>
  40. /// <remarks>
  41. /// Note that this being zero does not mean this damage has no effect. Healing in one type may cancel damage
  42. /// in another. Consider using <see cref="AnyPositive"/> or <see cref="Empty"/> instead.
  43. /// </remarks>
  44. public FixedPoint2 GetTotal()
  45. {
  46. var total = FixedPoint2.Zero;
  47. foreach (var value in DamageDict.Values)
  48. {
  49. total += value;
  50. }
  51. return total;
  52. }
  53. /// <summary>
  54. /// Returns true if the specifier contains any positive damage values.
  55. /// Differs from <see cref="Empty"/> as a damage specifier might contain entries with zeroes.
  56. /// This also returns false if the specifier only contains negative values.
  57. /// </summary>
  58. public bool AnyPositive()
  59. {
  60. foreach (var value in DamageDict.Values)
  61. {
  62. if (value > FixedPoint2.Zero)
  63. return true;
  64. }
  65. return false;
  66. }
  67. /// <summary>
  68. /// Whether this damage specifier has any entries.
  69. /// </summary>
  70. [JsonIgnore]
  71. public bool Empty => DamageDict.Count == 0;
  72. #region constructors
  73. /// <summary>
  74. /// Constructor that just results in an empty dictionary.
  75. /// </summary>
  76. public DamageSpecifier() { }
  77. /// <summary>
  78. /// Constructor that takes another DamageSpecifier instance and copies it.
  79. /// </summary>
  80. public DamageSpecifier(DamageSpecifier damageSpec)
  81. {
  82. DamageDict = new(damageSpec.DamageDict);
  83. }
  84. /// <summary>
  85. /// Constructor that takes a single damage type prototype and a damage value.
  86. /// </summary>
  87. public DamageSpecifier(DamageTypePrototype type, FixedPoint2 value)
  88. {
  89. DamageDict = new() { { type.ID, value } };
  90. }
  91. /// <summary>
  92. /// Constructor that takes a single damage group prototype and a damage value. The value is divided between members of the damage group.
  93. /// </summary>
  94. public DamageSpecifier(DamageGroupPrototype group, FixedPoint2 value)
  95. {
  96. // Simply distribute evenly (except for rounding).
  97. // We do this by reducing remaining the # of types and damage every loop.
  98. var remainingTypes = group.DamageTypes.Count;
  99. var remainingDamage = value;
  100. foreach (var damageType in group.DamageTypes)
  101. {
  102. var damage = remainingDamage / FixedPoint2.New(remainingTypes);
  103. DamageDict.Add(damageType, damage);
  104. remainingDamage -= damage;
  105. remainingTypes -= 1;
  106. }
  107. }
  108. #endregion constructors
  109. /// <summary>
  110. /// Reduce (or increase) damages by applying a damage modifier set.
  111. /// </summary>
  112. /// <remarks>
  113. /// Only applies resistance to a damage type if it is dealing damage, not healing.
  114. /// This will never convert damage into healing.
  115. /// </remarks>
  116. public static DamageSpecifier ApplyModifierSet(DamageSpecifier damageSpec, DamageModifierSet modifierSet)
  117. {
  118. // Make a copy of the given data. Don't modify the one passed to this function. I did this before, and weapons became
  119. // duller as you hit walls. Neat, but not FixedPoint2ended. And confusing, when you realize your fists don't work no
  120. // more cause they're just bloody stumps.
  121. DamageSpecifier newDamage = new();
  122. newDamage.DamageDict.EnsureCapacity(damageSpec.DamageDict.Count);
  123. foreach (var (key, value) in damageSpec.DamageDict)
  124. {
  125. if (value == 0)
  126. continue;
  127. if (value < 0)
  128. {
  129. newDamage.DamageDict[key] = value;
  130. continue;
  131. }
  132. float newValue = value.Float();
  133. if (modifierSet.FlatReduction.TryGetValue(key, out var reduction))
  134. newValue = Math.Max(0f, newValue - reduction); // flat reductions can't heal you
  135. if (modifierSet.Coefficients.TryGetValue(key, out var coefficient))
  136. newValue *= coefficient; // coefficients can heal you, e.g. cauterizing bleeding
  137. if(newValue != 0)
  138. newDamage.DamageDict[key] = FixedPoint2.New(newValue);
  139. }
  140. return newDamage;
  141. }
  142. /// <summary>
  143. /// Reduce (or increase) damages by applying multiple modifier sets.
  144. /// </summary>
  145. /// <param name="damageSpec"></param>
  146. /// <param name="modifierSets"></param>
  147. /// <returns></returns>
  148. public static DamageSpecifier ApplyModifierSets(DamageSpecifier damageSpec, IEnumerable<DamageModifierSet> modifierSets)
  149. {
  150. bool any = false;
  151. DamageSpecifier newDamage = damageSpec;
  152. foreach (var set in modifierSets)
  153. {
  154. // This creates a new damageSpec for each modifier when we really onlt need to create one.
  155. // This is quite inefficient, but hopefully this shouldn't ever be called frequently.
  156. newDamage = ApplyModifierSet(newDamage, set);
  157. any = true;
  158. }
  159. if (!any)
  160. newDamage = new DamageSpecifier(damageSpec);
  161. return newDamage;
  162. }
  163. /// <summary>
  164. /// Remove any damage entries with zero damage.
  165. /// </summary>
  166. public void TrimZeros()
  167. {
  168. foreach (var (key, value) in DamageDict)
  169. {
  170. if (value == 0)
  171. {
  172. DamageDict.Remove(key);
  173. }
  174. }
  175. }
  176. /// <summary>
  177. /// Clamps each damage value to be within the given range.
  178. /// </summary>
  179. public void Clamp(FixedPoint2 minValue, FixedPoint2 maxValue)
  180. {
  181. DebugTools.Assert(minValue < maxValue);
  182. ClampMax(maxValue);
  183. ClampMin(minValue);
  184. }
  185. /// <summary>
  186. /// Sets all damage values to be at least as large as the given number.
  187. /// </summary>
  188. /// <remarks>
  189. /// Note that this only acts on damage types present in the dictionary. It will not add new damage types.
  190. /// </remarks>
  191. public void ClampMin(FixedPoint2 minValue)
  192. {
  193. foreach (var (key, value) in DamageDict)
  194. {
  195. if (value < minValue)
  196. {
  197. DamageDict[key] = minValue;
  198. }
  199. }
  200. }
  201. /// <summary>
  202. /// Sets all damage values to be at most some number. Note that if a damage type is not present in the
  203. /// dictionary, these will not be added.
  204. /// </summary>
  205. public void ClampMax(FixedPoint2 maxValue)
  206. {
  207. foreach (var (key, value) in DamageDict)
  208. {
  209. if (value > maxValue)
  210. {
  211. DamageDict[key] = maxValue;
  212. }
  213. }
  214. }
  215. /// <summary>
  216. /// This adds the damage values of some other <see cref="DamageSpecifier"/> to the current one without
  217. /// adding any new damage types.
  218. /// </summary>
  219. /// <remarks>
  220. /// This is used for <see cref="DamageableComponent"/>s, such that only "supported" damage types are
  221. /// actually added to the component. In most other instances, you can just use the addition operator.
  222. /// </remarks>
  223. public void ExclusiveAdd(DamageSpecifier other)
  224. {
  225. foreach (var (type, value) in other.DamageDict)
  226. {
  227. // CollectionsMarshal my beloved.
  228. if (DamageDict.TryGetValue(type, out var existing))
  229. {
  230. DamageDict[type] = existing + value;
  231. }
  232. }
  233. }
  234. /// <summary>
  235. /// Add up all the damage values for damage types that are members of a given group.
  236. /// </summary>
  237. /// <remarks>
  238. /// If no members of the group are included in this specifier, returns false.
  239. /// </remarks>
  240. public bool TryGetDamageInGroup(DamageGroupPrototype group, out FixedPoint2 total)
  241. {
  242. bool containsMemeber = false;
  243. total = FixedPoint2.Zero;
  244. foreach (var type in group.DamageTypes)
  245. {
  246. if (DamageDict.TryGetValue(type, out var value))
  247. {
  248. total += value;
  249. containsMemeber = true;
  250. }
  251. }
  252. return containsMemeber;
  253. }
  254. /// <summary>
  255. /// Returns a dictionary using <see cref="DamageGroupPrototype.ID"/> keys, with values calculated by adding
  256. /// up the values for each damage type in that group
  257. /// </summary>
  258. /// <remarks>
  259. /// If a damage type is associated with more than one supported damage group, it will contribute to the
  260. /// total of each group. If no members of a group are present in this <see cref="DamageSpecifier"/>, the
  261. /// group is not included in the resulting dictionary.
  262. /// </remarks>
  263. public Dictionary<string, FixedPoint2> GetDamagePerGroup(IPrototypeManager protoManager)
  264. {
  265. var dict = new Dictionary<string, FixedPoint2>();
  266. GetDamagePerGroup(protoManager, dict);
  267. return dict;
  268. }
  269. /// <inheritdoc cref="GetDamagePerGroup(Robust.Shared.Prototypes.IPrototypeManager)"/>
  270. public void GetDamagePerGroup(IPrototypeManager protoManager, Dictionary<string, FixedPoint2> dict)
  271. {
  272. dict.Clear();
  273. foreach (var group in protoManager.EnumeratePrototypes<DamageGroupPrototype>())
  274. {
  275. if (TryGetDamageInGroup(group, out var value))
  276. dict.Add(group.ID, value);
  277. }
  278. }
  279. #region Operators
  280. public static DamageSpecifier operator *(DamageSpecifier damageSpec, FixedPoint2 factor)
  281. {
  282. DamageSpecifier newDamage = new();
  283. foreach (var entry in damageSpec.DamageDict)
  284. {
  285. newDamage.DamageDict.Add(entry.Key, entry.Value * factor);
  286. }
  287. return newDamage;
  288. }
  289. public static DamageSpecifier operator *(DamageSpecifier damageSpec, float factor)
  290. {
  291. DamageSpecifier newDamage = new();
  292. foreach (var entry in damageSpec.DamageDict)
  293. {
  294. newDamage.DamageDict.Add(entry.Key, entry.Value * factor);
  295. }
  296. return newDamage;
  297. }
  298. public static DamageSpecifier operator /(DamageSpecifier damageSpec, FixedPoint2 factor)
  299. {
  300. DamageSpecifier newDamage = new();
  301. foreach (var entry in damageSpec.DamageDict)
  302. {
  303. newDamage.DamageDict.Add(entry.Key, entry.Value / factor);
  304. }
  305. return newDamage;
  306. }
  307. public static DamageSpecifier operator /(DamageSpecifier damageSpec, float factor)
  308. {
  309. DamageSpecifier newDamage = new();
  310. foreach (var entry in damageSpec.DamageDict)
  311. {
  312. newDamage.DamageDict.Add(entry.Key, entry.Value / factor);
  313. }
  314. return newDamage;
  315. }
  316. public static DamageSpecifier operator +(DamageSpecifier damageSpecA, DamageSpecifier damageSpecB)
  317. {
  318. // Copy existing dictionary from dataA
  319. DamageSpecifier newDamage = new(damageSpecA);
  320. // Then just add types in B
  321. foreach (var entry in damageSpecB.DamageDict)
  322. {
  323. if (!newDamage.DamageDict.TryAdd(entry.Key, entry.Value))
  324. {
  325. // Key already exists, add values
  326. newDamage.DamageDict[entry.Key] += entry.Value;
  327. }
  328. }
  329. return newDamage;
  330. }
  331. // Here we define the subtraction operator explicitly, rather than implicitly via something like X + (-1 * Y).
  332. // This is faster because FixedPoint2 multiplication is somewhat involved.
  333. public static DamageSpecifier operator -(DamageSpecifier damageSpecA, DamageSpecifier damageSpecB)
  334. {
  335. DamageSpecifier newDamage = new(damageSpecA);
  336. foreach (var entry in damageSpecB.DamageDict)
  337. {
  338. if (!newDamage.DamageDict.TryAdd(entry.Key, -entry.Value))
  339. {
  340. newDamage.DamageDict[entry.Key] -= entry.Value;
  341. }
  342. }
  343. return newDamage;
  344. }
  345. public static DamageSpecifier operator +(DamageSpecifier damageSpec) => damageSpec;
  346. public static DamageSpecifier operator -(DamageSpecifier damageSpec) => damageSpec * -1;
  347. public static DamageSpecifier operator *(float factor, DamageSpecifier damageSpec) => damageSpec * factor;
  348. public static DamageSpecifier operator *(FixedPoint2 factor, DamageSpecifier damageSpec) => damageSpec * factor;
  349. public bool Equals(DamageSpecifier? other)
  350. {
  351. if (other == null || DamageDict.Count != other.DamageDict.Count)
  352. return false;
  353. foreach (var (key, value) in DamageDict)
  354. {
  355. if (!other.DamageDict.TryGetValue(key, out var otherValue) || value != otherValue)
  356. return false;
  357. }
  358. return true;
  359. }
  360. public FixedPoint2 this[string key] => DamageDict[key];
  361. }
  362. #endregion
  363. }