ECS damageable (#4529)

* ECS and damage Data

* Comments and newlines

* Added Comments

* Make TryChangeDamageEvent immutable

* Remove SetAllDamage event

Use public SetAllDamage function instead

* Undo destructible mistakes

That was some shit code.

* Rename DamageData to DamageSpecifier

And misc small edits

misc

* Cache trigger prototypes.

* Renaming destructible classes & functions

* Revert "Cache trigger prototypes."

This reverts commit 86bae15ba6616884dba75f552dfdfbe2d1fb6586.

* Replace prototypes with prototype IDs.

* Split damage.yml into individual files

* move get/handle component state to system

* Update HealthChange doc

* Make godmode call Dirty() on damageable component

* Add Initialize() to fix damage test

* Make non-static

* uncache resistance set prototype and trim DamageableComponentState

* Remove unnecessary Dirty() calls during initialization

* RemoveTryChangeDamageEvent

* revert Dirty()

* Fix MobState relying on DamageableComponent.Dirty()

* Fix DisposalUnit Tests.

These were previously failing, but because the async was not await-ed, this never raised the exception.

After I fixed MobState component, this exception stopped happening and instead the assertions started being tested & failing

* Disposal test 2: electric boogaloo

* Fix typos/mistakes

also add comments and fix spacing.

* Use Uids instead of IEntity

* fix merge

* Comments, a merge issue, and making some damage ignore resistances

* Extend DamageSpecifier and use it for DamageableComponent

* fix master merge

* Fix Disposal unit test. Again.

Snapgrids were removed in master

* Execute Exectute
This commit is contained in:
Leon Friedrich
2021-09-15 03:07:37 +10:00
committed by GitHub
parent 22cc42ff50
commit df584ad446
212 changed files with 2876 additions and 3441 deletions

View File

@@ -1,499 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Shared.Acts;
using Content.Shared.Damage.Container;
using Content.Shared.Damage.Resistances;
using Content.Shared.Radiation;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Players;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Shared.Damage.Components
{
/// <summary>
/// Component that allows attached entities to take damage.
/// </summary>
/// <remarks>
/// The supported damage types are specified using a <see cref="DamageContainerPrototype"/>s. DamageContainers
/// are effectively a dictionary of damage types and damage numbers, along with functions to modify them. Damage
/// groups are collections of damage types. A damage group is 'applicable' to a damageable component if it
/// supports at least one damage type in that group. A subset of these groups may be 'fully supported' when every
/// member of the group is supported by the container. This basic version never dies (thus can take an
/// indefinite amount of damage).
/// </remarks>
[RegisterComponent]
[ComponentReference(typeof(IDamageableComponent))]
[NetworkedComponent()]
public class DamageableComponent : Component, IDamageableComponent, IRadiationAct, ISerializationHooks
{
public override string Name => "Damageable";
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
/// <summary>
/// The main damage dictionary. All the damage information is stored in this dictionary with <see cref="DamageTypePrototype"/> keys.
/// </summary>
private Dictionary<DamageTypePrototype, int> _damageDict = new();
[DataField("resistances")]
public string ResistanceSetId { get; set; } = "defaultResistances";
[ViewVariables] public ResistanceSet Resistances { get; set; } = new();
// TODO DAMAGE Use as default values, specify overrides in a separate property through yaml for better (de)serialization
[ViewVariables]
[DataField("damageContainer")]
public string DamageContainerId { get; set; } = "metallicDamageContainer";
// TODO DAMAGE Cache this
// When moving logic from damageableComponent --> Damage System, make damageSystem update these on damage change.
[ViewVariables] public int TotalDamage => _damageDict.Values.Sum();
[ViewVariables] public IReadOnlyDictionary<DamageTypePrototype, int> GetDamagePerType => _damageDict;
[ViewVariables] public IReadOnlyDictionary<DamageGroupPrototype, int> GetDamagePerApplicableGroup => DamageTypeDictToDamageGroupDict(_damageDict, ApplicableDamageGroups);
[ViewVariables] public IReadOnlyDictionary<DamageGroupPrototype, int> GetDamagePerFullySupportedGroup => DamageTypeDictToDamageGroupDict(_damageDict, FullySupportedDamageGroups);
// Whenever sending over network, also need a <string, int> dictionary
// TODO DAMAGE MAYBE Cache this?
public IReadOnlyDictionary<string, int> GetDamagePerApplicableGroupIDs => ConvertDictKeysToIDs(GetDamagePerApplicableGroup);
public IReadOnlyDictionary<string, int> GetDamagePerFullySupportedGroupIDs => ConvertDictKeysToIDs(GetDamagePerFullySupportedGroup);
public IReadOnlyDictionary<string, int> GetDamagePerTypeIDs => ConvertDictKeysToIDs(_damageDict);
// TODO PROTOTYPE Replace these datafield variables with prototype references, once they are supported.
// Also requires appropriate changes in OnExplosion() and RadiationAct()
[ViewVariables]
[DataField("radiationDamageTypes")]
public List<string> RadiationDamageTypeIDs { get; set; } = new() {"Radiation"};
[ViewVariables]
[DataField("explosionDamageTypes")]
public List<string> ExplosionDamageTypeIDs { get; set; } = new() { "Piercing", "Heat" };
public HashSet<DamageGroupPrototype> ApplicableDamageGroups { get; } = new();
public HashSet<DamageGroupPrototype> FullySupportedDamageGroups { get; } = new();
public HashSet<DamageTypePrototype> SupportedDamageTypes { get; } = new();
protected override void Initialize()
{
base.Initialize();
// TODO DAMAGE Serialize damage done and resistance changes
var damageContainerPrototype = _prototypeManager.Index<DamageContainerPrototype>(DamageContainerId);
ApplicableDamageGroups.Clear();
FullySupportedDamageGroups.Clear();
SupportedDamageTypes.Clear();
//Get Damage groups/types from the DamageContainerPrototype.
DamageContainerId = damageContainerPrototype.ID;
ApplicableDamageGroups.UnionWith(damageContainerPrototype.ApplicableDamageGroups);
FullySupportedDamageGroups.UnionWith(damageContainerPrototype.FullySupportedDamageGroups);
SupportedDamageTypes.UnionWith(damageContainerPrototype.SupportedDamageTypes);
//initialize damage dictionary 0 damage
_damageDict = new(SupportedDamageTypes.Count);
foreach (var type in SupportedDamageTypes)
{
_damageDict.Add(type, 0);
}
Resistances = new ResistanceSet(_prototypeManager.Index<ResistanceSetPrototype>(ResistanceSetId));
}
protected override void Startup()
{
base.Startup();
ForceHealthChangedEvent();
}
public override ComponentState GetComponentState(ICommonSession player)
{
return new DamageableComponentState(GetDamagePerTypeIDs);
}
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
{
base.HandleComponentState(curState, nextState);
if (!(curState is DamageableComponentState state))
{
return;
}
_damageDict.Clear();
foreach (var (type, damage) in state.DamageDict)
{
_damageDict[_prototypeManager.Index<DamageTypePrototype>(type)] = damage;
}
}
public int GetDamage(DamageTypePrototype type)
{
return GetDamagePerType.GetValueOrDefault(type);
}
public bool TryGetDamage(DamageTypePrototype type, out int damage)
{
return GetDamagePerType.TryGetValue(type, out damage);
}
public int GetDamage(DamageGroupPrototype group)
{
return GetDamagePerApplicableGroup.GetValueOrDefault(group);
}
public bool TryGetDamage(DamageGroupPrototype group, out int damage)
{
return GetDamagePerApplicableGroup.TryGetValue(group, out damage);
}
public bool IsApplicableDamageGroup(DamageGroupPrototype group)
{
return ApplicableDamageGroups.Contains(group);
}
public bool IsFullySupportedDamageGroup(DamageGroupPrototype group)
{
return FullySupportedDamageGroups.Contains(group);
}
public bool IsSupportedDamageType(DamageTypePrototype type)
{
return SupportedDamageTypes.Contains(type);
}
public bool TrySetDamage(DamageGroupPrototype group, int newValue)
{
if (!ApplicableDamageGroups.Contains(group))
{
return false;
}
if (newValue < 0)
{
// invalid value
return false;
}
foreach (var type in group.DamageTypes)
{
TrySetDamage(type, newValue);
}
return true;
}
public bool TrySetAllDamage(int newValue)
{
if (newValue < 0)
{
// invalid value
return false;
}
foreach (var type in SupportedDamageTypes)
{
TrySetDamage(type, newValue);
}
return true;
}
public bool TryChangeDamage(DamageTypePrototype type, int amount, bool ignoreDamageResistances = false)
{
// Check if damage type is supported, and get the current value if it is.
if (!GetDamagePerType.TryGetValue(type, out var current))
{
return false;
}
if (amount == 0)
{
return false;
}
// Apply resistances (does nothing if amount<0)
var finalDamage = amount;
if (!ignoreDamageResistances)
{
finalDamage = Resistances.CalculateDamage(type, amount);
}
if (finalDamage == 0)
return false;
// Are we healing below zero?
if (current + finalDamage < 0)
{
if (current == 0)
// Damage type is supported, but there is nothing to do
return false;
// Cap healing down to zero
_damageDict[type] = 0;
finalDamage = -current;
}
else
{
_damageDict[type] = current + finalDamage;
}
current = _damageDict[type];
var datum = new DamageChangeData(type, current, finalDamage);
var data = new List<DamageChangeData> {datum};
OnHealthChanged(data);
return true;
}
public bool TryChangeDamage(DamageGroupPrototype group, int amount, bool ignoreDamageResistances = false)
{
var types = group.DamageTypes.ToArray();
if (amount < 0)
{
// We are Healing. Keep track of how much we can hand out (with a better var name for readability).
var availableHealing = -amount;
// Get total group damage.
var damageToHeal = GetDamagePerApplicableGroup[group];
// Is there any damage to even heal?
if (damageToHeal == 0)
return false;
// If total healing is more than there is damage, just set to 0 and return.
if (damageToHeal <= availableHealing)
{
TrySetDamage(group, 0);
return true;
}
// Partially heal each damage group
int healing, damage;
foreach (var type in types)
{
if (!_damageDict.TryGetValue(type, out damage))
{
// Damage Type is not supported. Continue without reducing availableHealing
continue;
}
// Apply healing to the damage type. The healing amount may be zero if either damage==0, or if
// integer rounding made it zero (i.e., damage is small)
healing = (availableHealing * damage) / damageToHeal;
TryChangeDamage(type, -healing, ignoreDamageResistances);
// remove this damage type from the damage we consider for future loops, regardless of how much we
// actually healed this type.
damageToHeal -= damage;
availableHealing -= healing;
// If we now healed all the damage, exit. otherwise 1/0 and universe explodes.
if (damageToHeal == 0)
{
break;
}
}
// Damage type is supported, there was damage to heal, and resistances were ignored
// --> Damage must have changed
return true;
}
else if (amount > 0)
{
// Resistances may result in no actual damage change. We need to keep track if any damage got through.
var damageChanged = false;
// We are adding damage. Keep track of how much we can dish out (with a better var name for readability).
var availableDamage = amount;
// How many damage types do we have to distribute over?.
var numberDamageTypes = types.Length;
// Apply damage to each damage group
int damage;
foreach (var type in types)
{
// Distribute the remaining damage over the remaining damage types.
damage = availableDamage / numberDamageTypes;
// Try apply the damage type. If damage type is not supported, this has no effect.
// We also use the return value to check whether any damage has changed
damageChanged = TryChangeDamage(type, damage, ignoreDamageResistances) || damageChanged;
// regardless of whether we dealt damage, reduce the amount to distribute.
availableDamage -= damage;
numberDamageTypes -= 1;
}
return damageChanged;
}
// amount==0 no damage change.
return false;
}
public bool TrySetDamage(DamageTypePrototype type, int newValue)
{
if (!_damageDict.TryGetValue(type, out var oldValue))
{
return false;
}
if (newValue < 0)
{
// invalid value
return false;
}
if (oldValue == newValue)
{
// No health change.
// But we are trying to set, not trying to change.
return true;
}
_damageDict[type] = newValue;
var delta = newValue - oldValue;
var datum = new DamageChangeData(type, 0, delta);
var data = new List<DamageChangeData> {datum};
OnHealthChanged(data);
return true;
}
public void ForceHealthChangedEvent()
{
var data = new List<DamageChangeData>();
foreach (var type in SupportedDamageTypes)
{
var damage = GetDamage(type);
var datum = new DamageChangeData(type, damage, 0);
data.Add(datum);
}
OnHealthChanged(data);
}
private void OnHealthChanged(List<DamageChangeData> changes)
{
var args = new DamageChangedEventArgs(this, changes);
OnHealthChanged(args);
}
protected virtual void OnHealthChanged(DamageChangedEventArgs e)
{
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, e);
var message = new DamageChangedMessage(this, e.Data);
SendMessage(message);
Dirty();
}
public void RadiationAct(float frameTime, SharedRadiationPulseComponent radiation)
{
var totalDamage = Math.Max((int)(frameTime * radiation.RadsPerSecond), 1);
foreach (var typeID in RadiationDamageTypeIDs)
{
TryChangeDamage(_prototypeManager.Index<DamageTypePrototype>(typeID), totalDamage);
}
}
public void OnExplosion(ExplosionEventArgs eventArgs)
{
var damage = eventArgs.Severity switch
{
ExplosionSeverity.Light => 20,
ExplosionSeverity.Heavy => 60,
ExplosionSeverity.Destruction => 250,
_ => throw new ArgumentOutOfRangeException()
};
foreach (var typeID in ExplosionDamageTypeIDs)
{
TryChangeDamage(_prototypeManager.Index<DamageTypePrototype>(typeID), damage);
}
}
/// <summary>
/// Take a dictionary with <see cref="IPrototype"/> keys and return a dictionary using <see cref="IPrototype.ID"/> as keys
/// instead.
/// </summary>
/// <remarks>
/// Useful when sending damage type and group prototypes dictionaries over the network.
/// </remarks>
public static IReadOnlyDictionary<string, int>
ConvertDictKeysToIDs<TPrototype>(IReadOnlyDictionary<TPrototype, int> prototypeDict)
where TPrototype : IPrototype
{
Dictionary<string, int> idDict = new(prototypeDict.Count);
foreach (var entry in prototypeDict)
{
idDict.Add(entry.Key.ID, entry.Value);
}
return idDict;
}
/// <summary>
/// Convert a dictionary with damage type keys to a dictionary of damage groups keys.
/// </summary>
/// <remarks>
/// Takes a dictionary with damage types as keys and integers as values, and an iterable list of damage
/// groups. Returns a dictionary with damage group keys, with values calculated by adding up the values for
/// each damage type in that group. If a damage type is associated with more than one supported damage
/// group, it will contribute to the total of each group. Conversely, some damage types may not contribute
/// to the new dictionary if their associated group(s) are not in given list of groups.
/// </remarks>
public static IReadOnlyDictionary<DamageGroupPrototype, int>
DamageTypeDictToDamageGroupDict(IReadOnlyDictionary<DamageTypePrototype, int> damageTypeDict, IEnumerable<DamageGroupPrototype> groupKeys)
{
var damageGroupDict = new Dictionary<DamageGroupPrototype, int>();
int damageGroupSumDamage, damageTypeDamage;
// iterate over the list of group keys for our new dictionary
foreach (var group in groupKeys)
{
// For each damage type in this group, add up the damage present in the given dictionary
damageGroupSumDamage = 0;
foreach (var type in group.DamageTypes)
{
// if the damage type is in the dictionary, add it's damage to the group total.
if (damageTypeDict.TryGetValue(type, out damageTypeDamage))
{
damageGroupSumDamage += damageTypeDamage;
}
}
damageGroupDict.Add(group, damageGroupSumDamage);
}
return damageGroupDict;
}
}
[Serializable, NetSerializable]
public class DamageableComponentState : ComponentState
{
public readonly IReadOnlyDictionary<string, int> DamageDict;
public DamageableComponentState(IReadOnlyDictionary<string, int> damageDict)
{
DamageDict = damageDict;
}
}
}

View File

@@ -1,225 +0,0 @@
using System.Collections.Generic;
using Content.Shared.Acts;
using Content.Shared.Damage.Resistances;
using Robust.Shared.GameObjects;
namespace Content.Shared.Damage.Components
{
public interface IDamageableComponent : IComponent, IExAct
{
/// <summary>
/// The sum of all damages types in the DamageableComponent.
/// </summary>
int TotalDamage { get; }
/// <summary>
/// Returns a dictionary of the damage in the container, indexed by applicable <see cref="DamageGroupPrototype"/>.
/// </summary>
/// <remarks>
/// The values represent the sum of all damage in each group. If a supported damage type is a member of more than one group, it will contribute to each one.
/// Therefore, the sum of the values may be greater than the sum of the values in the dictionary returned by <see cref="GetDamagePerType"/>
/// </remarks>
IReadOnlyDictionary<DamageGroupPrototype, int> GetDamagePerApplicableGroup { get; }
/// <summary>
/// Returns a dictionary of the damage in the container, indexed by fully supported instances of <see
/// cref="DamageGroupPrototype"/>.
/// </summary>
/// <remarks>
/// The values represent the sum of all damage in each group. As the damage container may have some damage
/// types that are not part of a fully supported damage group, the sum of the values may be less of the values
/// in the dictionary returned by <see cref="GetDamagePerType"/>. On the other hand, if a supported damage type
/// is a member of more than one group, it will contribute to each one. Therefore, the sum may also be greater
/// instead.
/// </remarks>
IReadOnlyDictionary<DamageGroupPrototype, int> GetDamagePerFullySupportedGroup { get; }
/// <summary>
/// Returns a dictionary of the damage in the container, indexed by <see cref="DamageTypePrototype"/>.
/// </summary>
IReadOnlyDictionary<DamageTypePrototype, int> GetDamagePerType { get; }
/// <summary>
/// Like <see cref="GetDamagePerApplicableGroup"/>, but indexed by <see cref="DamageGroupPrototype.ID"/>
/// </summary>
IReadOnlyDictionary<string, int> GetDamagePerApplicableGroupIDs { get; }
/// <summary>
/// Like <see cref="GetDamagePerFullySupportedGroup"/>, but indexed by <see cref="DamageGroupPrototype.ID"/>
/// </summary>
IReadOnlyDictionary<string, int> GetDamagePerFullySupportedGroupIDs { get; }
/// <summary>
/// Like <see cref="GetDamagePerType"/>, but indexed by <see cref="DamageTypePrototype.ID"/>
/// </summary>
IReadOnlyDictionary<string, int> GetDamagePerTypeIDs { get; }
/// <summary>
/// Collection of damage types supported by this DamageableComponent.
/// </summary>
/// <remarks>
/// Each of these damage types is fully supported. If any of these damage types is a
/// member of a damage group, these groups are represented in <see cref="ApplicableDamageGroups"></see>
/// </remarks>
HashSet<DamageTypePrototype> SupportedDamageTypes { get; }
/// <summary>
/// Collection of damage groups that are fully supported by DamageableComponent.
/// </summary>
/// <remarks>
/// This describes what damage groups this damage container explicitly supports. It supports every damage type
/// contained in these damage groups. It may also support other damage types not in these groups. To see all
/// damage types <see cref="SupportedDamageTypes"/>, and to see all applicable damage groups <see
/// cref="ApplicableDamageGroups"/>.
/// </remarks>
HashSet<DamageGroupPrototype> FullySupportedDamageGroups { get; }
/// <summary>
/// Collection of damage groups that could apply damage to this DamageableComponent.
/// </summary>
/// <remarks>
/// This describes what damage groups could have an effect on this damage container. However not every damage
/// group has to be fully supported. For example, the container may support ONLY the piercing damage type. It should
/// therefore be affected by instances of brute damage, but does not necessarily support blunt or slash damage.
/// For a list of supported damage types, see <see cref="SupportedDamageTypes"/>.
/// </remarks>
HashSet<DamageGroupPrototype> ApplicableDamageGroups { get; }
/// <summary>
/// The resistances of this component.
/// </summary>
ResistanceSet Resistances { get; }
/// <summary>
/// Tries to get the amount of damage of a type.
/// </summary>
/// <param name="type">The type to get the damage of.</param>
/// <param name="damage">The amount of damage of that type.</param>
/// <returns>
/// True if the given <see cref="type"/> is supported, false otherwise.
/// </returns>
bool TryGetDamage(DamageTypePrototype type, out int damage);
/// <summary>
/// Returns the amount of damage of a given type, or zero if it is not supported.
/// </summary>
int GetDamage(DamageTypePrototype type);
/// <summary>
/// Tries to get the total amount of damage in a damage group.
/// </summary>
/// <param name="group">The group to get the damage of.</param>
/// <param name="damage">The amount of damage in that group.</param>
/// <returns>
/// True if the given group is applicable to this container, false otherwise.
/// </returns>
bool TryGetDamage(DamageGroupPrototype group, out int damage);
/// <summary>
/// Returns the amount of damage present in an applicable group, or zero if no members are supported.
/// </summary>
int GetDamage(DamageGroupPrototype group);
/// <summary>
/// Tries to change the specified <see cref="DamageTypePrototype"/>, applying
/// resistance values only if it is dealing damage.
/// </summary>
/// <param name="type">Type of damage being changed.</param>
/// <param name="amount">
/// Amount of damage being received (positive for damage, negative for heals).
/// </param>
/// <param name="ignoreDamageResistances">
/// Whether or not to ignore resistances when taking damage.
/// Healing always ignores resistances, regardless of this input.
/// </param>
/// <returns>
/// False if the given type is not supported or no damage change occurred; true otherwise.
/// </returns>
bool TryChangeDamage(DamageTypePrototype type, int amount, bool ignoreDamageResistances = false);
/// <summary>
/// Tries to change damage of the specified <see cref="DamageGroupPrototype"/>, applying resistance values
/// only if it is damage.
/// </summary>
/// <remarks>
/// <para>
/// If dealing damage, this spreads the damage change amount evenly between the <see
/// cref="DamageTypePrototype"></see>s in this group (subject to integer rounding). If only a subset of the
/// damage types in the group are actually supported, then the total damage dealt may be less than expected
/// (unsupported damage is ignored).
/// </para>
/// <para>
/// If healing damage, this spreads the damage change proportional to the current damage value of each <see
/// cref="DamageTypePrototype"></see> (subject to integer rounding). If there is less damage than is being
/// healed, some healing is wasted. Unsupported damage types do not waste healing.
/// </para>
/// </remarks>
/// <param name="group">group of damage being changed.</param>
/// <param name="amount">
/// Amount of damage being received (positive for damage, negative for heals).
/// </param>
/// <param name="ignoreDamageResistances">
/// Whether to ignore resistances when taking damage. Healing always ignores resistances, regardless of this
/// input.
/// </param>
/// <returns>
/// Returns false if the given group is not applicable or no damage change occurred; true otherwise.
/// </returns>
bool TryChangeDamage(DamageGroupPrototype group, int amount, bool ignoreDamageResistances = false);
/// <summary>
/// Forcefully sets the specified <see cref="DamageTypePrototype"/> to the given value, ignoring resistance
/// values.
/// </summary>
/// <param name="type">Type of damage being set.</param>
/// <param name="newValue">New damage value to be set.</param>
/// <returns>
/// Returns false if a given type is not supported or a negative value is provided; true otherwise.
/// </returns>
bool TrySetDamage(DamageTypePrototype type, int newValue);
/// <summary>
/// Forcefully sets all damage types in a specified damage group using <see cref="TrySetDamage"></see>.
/// </summary>
/// <remarks>
/// Note that the actual damage of this group will be equal to the given value times the number damage group
/// members that this container supports.
/// </remarks>
/// <param name="group">Group of damage being set.</param>
/// <param name="newValue">New damage value to be set.</param>
/// <returns>
/// Returns false if the given group is not applicable or a negative value is provided; true otherwise.
/// </returns>
bool TrySetDamage(DamageGroupPrototype group, int newValue);
/// <summary>
/// Sets all supported damage types to specified value using <see cref="TrySetDamage"></see>.
/// </summary>
/// <param name="newValue">New damage value to be set.</param>
/// <returns>
/// Returns false if a negative value is provided; true otherwise.
/// </returns>
bool TrySetAllDamage(int newValue);
/// <summary>
/// Returns true if the given damage group is applicable to this damage container.
/// </summary>
public bool IsApplicableDamageGroup(DamageGroupPrototype group);
/// <summary>
/// Returns true if the given damage group is fully supported by this damage container.
/// </summary>
public bool IsFullySupportedDamageGroup(DamageGroupPrototype group);
/// <summary>
/// Returns true if the given damage type is supported by this damage container.
/// </summary>
public bool IsSupportedDamageType(DamageTypePrototype type);
/// <summary>
/// Invokes the HealthChangedEvent with the current values of health.
/// </summary>
void ForceHealthChangedEvent();
}
}

View File

@@ -1,151 +0,0 @@
using System;
using System.Collections.Generic;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Shared.Damage.Container
{
/// <summary>
/// A damage container which can be used to specify support for various damage types.
/// </summary>
/// <remarks>
/// This is effectively just a list of damage types that can be specified in YAML files using both damage types
/// and damage groups. Currently this is only used to specify what damage types a <see
/// cref="Components.DamageableComponent"/> should support.
/// </remarks>
[Prototype("damageContainer")]
[Serializable, NetSerializable]
public class DamageContainerPrototype : IPrototype, ISerializationHooks
{
private IPrototypeManager _prototypeManager = default!;
[ViewVariables]
[DataField("id", required: true)]
public string ID { get; } = default!;
/// <summary>
/// Determines whether this DamageContainerPrototype will support ALL damage types and groups. If true,
/// ignore all other options.
/// </summary>
[DataField("supportAll")] private bool _supportAll;
[DataField("supportedGroups")] private HashSet<string> _supportedDamageGroupIDs = new();
[DataField("supportedTypes")] private HashSet<string> _supportedDamageTypeIDs = new();
private HashSet<DamageGroupPrototype> _applicableDamageGroups = new();
private HashSet<DamageGroupPrototype> _fullySupportedDamageGroups = new();
private HashSet<DamageTypePrototype> _supportedDamageTypes = new();
// TODO NET 5 IReadOnlySet
/// <summary>
/// Collection of damage groups that can affect this container.
/// </summary>
/// <remarks>
/// This describes what damage groups can have an effect on this damage container. However not every damage
/// group has to be fully supported. For example, the container may support ONLY the piercing damage type.
/// It should therefore be affected by instances of brute group damage, but does not necessarily support
/// blunt or slash damage. If damage containers are only specified by supported damage groups, and every
/// damage type is in only one damage group, then SupportedDamageTypes should be equal to
/// ApplicableDamageGroups. For a list of supported damage types, see <see cref="SupportedDamageTypes"/>.
/// </remarks>
[ViewVariables] public IReadOnlyCollection<DamageGroupPrototype> ApplicableDamageGroups => _applicableDamageGroups;
/// <summary>
/// Collection of damage groups that are fully supported by this container.
/// </summary>
/// <remarks>
/// This describes what damage groups this damage container explicitly supports. It supports every damage
/// type contained in these damage groups. It may also support other damage types not in these groups. To
/// see all damage types <see cref="SupportedDamageTypes"/>, and to see all applicable damage groups <see
/// cref="ApplicableDamageGroups"/>.
/// </remarks>
[ViewVariables] public IReadOnlyCollection<DamageGroupPrototype> FullySupportedDamageGroups => _fullySupportedDamageGroups;
/// <summary>
/// Collection of damage types supported by this container.
/// </summary>
/// <remarks>
/// Each of these damage types is fully supported by the DamageContainer. If any of these damage types is a
/// member of a damage group, these groups are added to <see cref="ApplicableDamageGroups"></see>
/// </remarks>
[ViewVariables] public IReadOnlyCollection<DamageTypePrototype> SupportedDamageTypes => _supportedDamageTypes;
void ISerializationHooks.AfterDeserialization()
{
_prototypeManager = IoCManager.Resolve<IPrototypeManager>();
if (_supportAll)
{
foreach (var group in _prototypeManager.EnumeratePrototypes<DamageGroupPrototype>())
{
_applicableDamageGroups.Add(group);
_fullySupportedDamageGroups.Add(group);
}
foreach (var type in _prototypeManager.EnumeratePrototypes<DamageTypePrototype>())
{
_supportedDamageTypes.Add(type);
}
return;
}
// Add fully supported damage groups
foreach (var groupID in _supportedDamageGroupIDs)
{
var group = _prototypeManager.Index<DamageGroupPrototype>(groupID);
_fullySupportedDamageGroups.Add(group);
foreach (var type in group.DamageTypes)
{
_supportedDamageTypes.Add(type);
}
}
// Add individual damage types, that are either not part of a group, or whose groups are (possibly) not fully supported
foreach (var supportedTypeID in _supportedDamageTypeIDs)
{
var type = _prototypeManager.Index<DamageTypePrototype>(supportedTypeID);
_supportedDamageTypes.Add(type);
}
// For whatever reason, someone may have listed all members of a group as supported instead of just listing
// the group as supported. Check for this.
foreach (var group in _prototypeManager.EnumeratePrototypes<DamageGroupPrototype>())
{
if (_fullySupportedDamageGroups.Contains(group))
{
continue;
}
// The group is not in the list of fully supported groups. Should it be?
var allMembersSupported = true;
foreach (var type in group.DamageTypes)
{
if (!_supportedDamageTypes.Contains(type))
{
// not all members are supported
allMembersSupported = false;
break;
}
}
if (allMembersSupported) {
// All members are supported. The silly goose should have just used a damage group.
_fullySupportedDamageGroups.Add(group);
}
}
// For each supported damage type, check whether it is in any existing group, If it is add it to _applicableDamageGroups
foreach (var type in _supportedDamageTypes)
{
foreach (var group in _prototypeManager.EnumeratePrototypes<DamageGroupPrototype>())
{
if (group.DamageTypes.Contains(type))
{
_applicableDamageGroups.Add(group);
}
}
}
}
}
}

View File

@@ -1,31 +0,0 @@
namespace Content.Shared.Damage
{
/// <summary>
/// Data class with information on how the value of a
/// single <see cref="DamageTypePrototype"/> has changed.
/// </summary>
public struct DamageChangeData
{
/// <summary>
/// Type of damage that changed.
/// </summary>
public DamageTypePrototype Type;
/// <summary>
/// The new current value for that damage.
/// </summary>
public int NewValue;
/// <summary>
/// How much the health value changed from its last value (negative is heals, positive is damage).
/// </summary>
public int Delta;
public DamageChangeData(DamageTypePrototype type, int newValue, int delta)
{
Type = type;
NewValue = newValue;
Delta = delta;
}
}
}

View File

@@ -1,35 +0,0 @@
using System;
using System.Collections.Generic;
using Content.Shared.Damage.Components;
namespace Content.Shared.Damage
{
public class DamageChangedEventArgs : EventArgs
{
public DamageChangedEventArgs(IDamageableComponent damageable, IReadOnlyList<DamageChangeData> data)
{
Damageable = damageable;
Data = data;
}
public DamageChangedEventArgs(IDamageableComponent damageable, DamageTypePrototype type, int newValue, int delta)
{
Damageable = damageable;
var datum = new DamageChangeData(type, newValue, delta);
var data = new List<DamageChangeData> {datum};
Data = data;
}
/// <summary>
/// Reference to the <see cref="IDamageableComponent"/> that invoked the event.
/// </summary>
public IDamageableComponent Damageable { get; }
/// <summary>
/// List containing data on each <see cref="DamageTypePrototype"/> that was changed.
/// </summary>
public IReadOnlyList<DamageChangeData> Data { get; }
}
}

View File

@@ -1,51 +0,0 @@
using System.Collections.Generic;
using Content.Shared.Damage.Components;
using Robust.Shared.GameObjects;
namespace Content.Shared.Damage
{
public class DamageChangedMessage : ComponentMessage
{
public DamageChangedMessage(IDamageableComponent damageable, IReadOnlyList<DamageChangeData> data)
{
Damageable = damageable;
Data = data;
}
public DamageChangedMessage(IDamageableComponent damageable, DamageTypePrototype type, int newValue, int delta)
{
Damageable = damageable;
var datum = new DamageChangeData(type, newValue, delta);
var data = new List<DamageChangeData> {datum};
Data = data;
}
/// <summary>
/// Reference to the <see cref="IDamageableComponent"/> that invoked the event.
/// </summary>
public IDamageableComponent Damageable { get; }
/// <summary>
/// List containing data on each <see cref="DamageTypePrototype"/> that was changed.
/// </summary>
public IReadOnlyList<DamageChangeData> Data { get; }
public bool TookDamage
{
get
{
foreach (var datum in Data)
{
if (datum.Delta > 0)
{
return true;
}
}
return false;
}
}
}
}

View File

@@ -1,42 +0,0 @@
using System;
using System.Collections.Generic;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Shared.Damage
{
/// <summary>
/// A Group of <see cref="DamageTypePrototype"/>s.
/// </summary>
/// <remarks>
/// These groups can be used to specify supported damage types of a <see
/// cref="Container.DamageContainerPrototype"/>, or to change/get/set damage in a <see
/// cref="Components.DamageableComponent"/>.
/// </remarks>
[Prototype("damageGroup")]
[Serializable, NetSerializable]
public class DamageGroupPrototype : IPrototype, ISerializationHooks
{
private IPrototypeManager _prototypeManager = default!;
[DataField("id", required: true)] public string ID { get; } = default!;
[DataField("damageTypes", required: true)]
public List<string> TypeIDs { get; } = default!;
public HashSet<DamageTypePrototype> DamageTypes { get; } = new();
// Create set of damage types
void ISerializationHooks.AfterDeserialization()
{
_prototypeManager = IoCManager.Resolve<IPrototypeManager>();
foreach (var typeID in TypeIDs)
{
DamageTypes.Add(_prototypeManager.Index<DamageTypePrototype>(typeID));
}
}
}
}

View File

@@ -0,0 +1,375 @@
using Content.Shared.Damage.Prototypes;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Content.Shared.Damage
{
// TODO DAMAGE UNITS Move this whole class away from, using integers. Also get rid of a lot of the rounding. Just
// use DamageUnit math operators.
/// <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]
public class DamageSpecifier
{
[DataField("types", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<int, DamageTypePrototype>))]
private readonly Dictionary<string,int>? _damageTypeDictionary;
[DataField("groups", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<int, DamageGroupPrototype>))]
private readonly Dictionary<string, int>? _damageGroupDictionary;
/// <summary>
/// Main DamageSpecifier dictionary. Most DamageSpecifier functions exist to somehow modifying this.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public Dictionary<string, int> DamageDict
{
get
{
if (_damageDict == null)
DeserializeDamage();
return _damageDict!;
}
set => _damageDict = value;
}
private Dictionary<string, int>? _damageDict;
/// <summary>
/// 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. For this purpose, you should instead use <see cref="TrimZeros()"/> and then check the <see
/// cref="Empty"/> property.
/// </remarks>
public int Total => DamageDict.Values.Sum();
/// <summary>
/// Whether this damage specifier has any entries.
/// </summary>
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, int 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, int value)
{
_damageGroupDictionary = new() { { group.ID, value } };
}
#endregion constructors
/// <summary>
/// Combines the damage group and type datafield dictionaries into a single damage dictionary.
/// </summary>
public void DeserializeDamage()
{
// Add all the damage types by just copying the type dictionary (if it is not null).
if (_damageTypeDictionary != null)
{
_damageDict = new(_damageTypeDictionary);
}
else
{
_damageDict = new();
}
if (_damageGroupDictionary == null)
return;
// Then resolve damage groups and add them
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
foreach (var entry in _damageGroupDictionary)
{
if (!prototypeManager.TryIndex<DamageGroupPrototype>(entry.Key, out var group))
{
// This can happen if deserialized before prototypes are loaded.
Logger.Error($"Unknown damage group given to DamageSpecifier: {entry.Key}");
continue;
}
// 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 = entry.Value;
foreach (var damageType in group.DamageTypes)
{
var damage = remainingDamage / remainingTypes;
if (!_damageDict.TryAdd(damageType, damage))
{
// Key already exists, add values
_damageDict[damageType] += damage;
}
remainingDamage -= damage;
remainingTypes -= 1;
}
}
}
/// <summary>
/// Reduce (or increase) damages by applying a resistance set.
/// </summary>
/// <remarks>
/// Only applies resistance to a damage type if it is dealing damage, not healing.
/// </remarks>
public static DamageSpecifier ApplyResistanceSet(DamageSpecifier damageSpec, ResistanceSetPrototype resistanceSet)
{
// 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 intended. And confusing, when you realize your fists don't work no
// more cause they're just bloody stumps.
DamageSpecifier newDamage = new(damageSpec);
foreach (var entry in newDamage.DamageDict)
{
if (entry.Value <= 0) continue;
float newValue = entry.Value;
if (resistanceSet.FlatReduction.TryGetValue(entry.Key, out var reduction))
{
newValue -= reduction;
if (newValue <= 0)
{
// flat reductions cannot heal you
newDamage.DamageDict[entry.Key] = 0;
continue;
}
}
if (resistanceSet.Coefficients.TryGetValue(entry.Key, out var coefficient))
{
// negative coefficients **can** heal you.
newValue = MathF.Round(newValue*coefficient, MidpointRounding.AwayFromZero);
}
newDamage.DamageDict[entry.Key] = (int) newValue;
}
newDamage.TrimZeros();
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(int minValue = 0, int maxValue = 0)
{
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(int minValue = 0)
{
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(int maxValue = 0)
{
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)
{
if (DamageDict.ContainsKey(type))
{
DamageDict[type] += 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 int total)
{
bool containsMemeber = false;
total = 0;
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, int> GetDamagePerGroup()
{
var damageGroupDict = new Dictionary<string, int>();
foreach (var group in IoCManager.Resolve<IPrototypeManager>().EnumeratePrototypes<DamageGroupPrototype>())
{
if (TryGetDamageInGroup(group, out var value))
{
damageGroupDict.Add(group.ID, value);
}
}
return damageGroupDict;
}
#region Operators
public static DamageSpecifier operator *(DamageSpecifier damageSpec, int 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, (int) MathF.Round(entry.Value * factor, MidpointRounding.AwayFromZero));
}
return newDamage;
}
public static DamageSpecifier operator /(DamageSpecifier damageSpec, int factor)
{
DamageSpecifier newDamage = new();
foreach (var entry in damageSpec.DamageDict)
{
newDamage.DamageDict.Add(entry.Key, (int) MathF.Round(entry.Value / (float) factor, MidpointRounding.AwayFromZero));
}
return newDamage;
}
public static DamageSpecifier operator /(DamageSpecifier damageSpec, float factor)
{
DamageSpecifier newDamage = new();
foreach (var entry in damageSpec.DamageDict)
{
newDamage.DamageDict.Add(entry.Key, (int) MathF.Round(entry.Value / factor, MidpointRounding.AwayFromZero));
}
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;
}
public static DamageSpecifier operator -(DamageSpecifier damageSpecA, DamageSpecifier damageSpecB) => damageSpecA + -damageSpecB;
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 *(int factor, DamageSpecifier damageSpec) => damageSpec * factor;
}
#endregion
}

View File

@@ -1,13 +0,0 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Shared.Damage
{
[UsedImplicitly]
public class DamageSystem : EntitySystem
{
}
}

View File

@@ -0,0 +1,129 @@
using System;
using System.Collections.Generic;
using Content.Shared.Acts;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Radiation;
using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
using Robust.Shared.ViewVariables;
namespace Content.Shared.Damage
{
/// <summary>
/// Component that allows entities to take damage.
/// </summary>
/// <remarks>
/// The supported damage types are specified using a <see cref="DamageContainerPrototype"/>s. DamageContainers
/// may also have resistances to certain damage types, defined via a <see cref="ResistanceSetPrototype"/>.
/// </remarks>
[RegisterComponent]
[NetworkedComponent()]
[Friend(typeof(DamageableSystem))]
public class DamageableComponent : Component, IRadiationAct, IExAct
{
public override string Name => "Damageable";
/// <summary>
/// This <see cref="DamageContainerPrototype"/> specifies what damage types are supported by this component.
/// If null, all damage types will be supported.
/// </summary>
[DataField("damageContainer", customTypeSerializer: typeof(PrototypeIdSerializer<DamageContainerPrototype>))]
public string? DamageContainerID;
/// <summary>
/// This <see cref="ResistanceSetPrototype"/> will be applied to any damage that is dealt to this container,
/// unless the damage explicitly ignores resistances.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("resistanceSet", customTypeSerializer: typeof(PrototypeIdSerializer<ResistanceSetPrototype>))]
public string? ResistanceSetID;
/// <summary>
/// All the damage information is stored in this <see cref="DamageSpecifier"/>.
/// </summary>
/// <remarks>
/// If this data-field is specified, this allows damageable components to be initialized with non-zero damage.
/// </remarks>
[DataField("damage")]
[ViewVariables(VVAccess.ReadWrite)]
public DamageSpecifier Damage = new();
/// <summary>
/// Damage, indexed by <see cref="DamageGroupPrototype"/> ID keys.
/// </summary>
/// <remarks>
/// Groups which have no members that are supported by this component will not be present in this
/// dictionary.
/// </remarks>
[ViewVariables] public Dictionary<string, int> DamagePerGroup = new();
/// <summary>
/// The sum of all damages in the DamageableComponent.
/// </summary>
[ViewVariables] public int TotalDamage;
// Really these shouldn't be here. OnExplosion() and RadiationAct() should be handled elsewhere.
[ViewVariables]
[DataField("radiationDamageTypes", customTypeSerializer: typeof(PrototypeIdListSerializer<DamageTypePrototype>))]
public List<string> RadiationDamageTypeIDs = new() {"Radiation"};
[ViewVariables]
[DataField("explosionDamageTypes", customTypeSerializer: typeof(PrototypeIdListSerializer<DamageTypePrototype>))]
public List<string> ExplosionDamageTypeIDs = new() { "Piercing", "Heat" };
// TODO RADIATION Remove this.
void IRadiationAct.RadiationAct(float frameTime, SharedRadiationPulseComponent radiation)
{
var damageValue = Math.Max((int) (frameTime * radiation.RadsPerSecond), 1);
// Radiation should really just be a damage group instead of a list of types.
DamageSpecifier damage = new();
foreach (var typeID in ExplosionDamageTypeIDs)
{
damage.DamageDict.Add(typeID, damageValue);
}
EntitySystem.Get<DamageableSystem>().TryChangeDamage(Owner.Uid, damage);
}
// TODO EXPLOSION Remove this.
void IExAct.OnExplosion(ExplosionEventArgs eventArgs)
{
var damageValue = eventArgs.Severity switch
{
ExplosionSeverity.Light => 20,
ExplosionSeverity.Heavy => 60,
ExplosionSeverity.Destruction => 250,
_ => throw new ArgumentOutOfRangeException()
};
// Explosion should really just be a damage group instead of a list of types.
DamageSpecifier damage = new();
foreach (var typeID in ExplosionDamageTypeIDs)
{
damage.DamageDict.Add(typeID, damageValue);
}
EntitySystem.Get<DamageableSystem>().TryChangeDamage(Owner.Uid, damage);
}
}
[Serializable, NetSerializable]
public class DamageableComponentState : ComponentState
{
public readonly Dictionary<string, int> DamageDict;
public readonly string? ResistanceSetID;
public DamageableComponentState(
Dictionary<string, int> damageDict,
string? resistanceSetID)
{
DamageDict = damageDict;
ResistanceSetID = resistanceSetID;
}
}
}

View File

@@ -0,0 +1,243 @@
using System.Collections.Generic;
using System.Linq;
using Content.Shared.Damage.Prototypes;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Prototypes;
namespace Content.Shared.Damage
{
public class DamageableSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public override void Initialize()
{
SubscribeLocalEvent<DamageableComponent, ComponentInit>(DamageableInit);
SubscribeLocalEvent<DamageableComponent, ComponentHandleState>(DamageableHandleState);
SubscribeLocalEvent<DamageableComponent, ComponentGetState>(DamageableGetState);
}
/// <summary>
/// Initialize a damageable component
/// </summary>
private void DamageableInit(EntityUid uid, DamageableComponent component, ComponentInit _)
{
if (component.DamageContainerID != null &&
_prototypeManager.TryIndex<DamageContainerPrototype>(component.DamageContainerID,
out var damageContainerPrototype))
{
// Initialize damage dictionary, using the types and groups from the damage
// container prototype
foreach (var type in damageContainerPrototype.SupportedTypes)
{
component.Damage.DamageDict.TryAdd(type, 0);
}
foreach (var groupID in damageContainerPrototype.SupportedGroups)
{
var group = _prototypeManager.Index<DamageGroupPrototype>(groupID);
foreach (var type in group.DamageTypes)
{
component.Damage.DamageDict.TryAdd(type, 0);
}
}
}
else
{
// No DamageContainerPrototype was given. So we will allow the container to support all damage types
foreach (var type in _prototypeManager.EnumeratePrototypes<DamageTypePrototype>())
{
component.Damage.DamageDict.TryAdd(type.ID, 0);
}
}
component.DamagePerGroup = component.Damage.GetDamagePerGroup();
component.TotalDamage = component.Damage.Total;
}
/// <summary>
/// Directly sets the damage specifier of a damageable component.
/// </summary>
/// <remarks>
/// Useful for some unfriendly folk. Also ensures that cached values are updated and that a damage changed
/// event is raised.
/// </remarks>
public void SetDamage(DamageableComponent damageable, DamageSpecifier damage)
{
damageable.Damage = damage;
DamageChanged(damageable);
}
/// <summary>
/// If the damage in a DamageableComponent was changed, this function should be called.
/// </summary>
/// <remarks>
/// This updates cached damage information, flags the component as dirty, and raises a damage changed event.
/// The damage changed event is used by other systems, such as damage thresholds.
/// </remarks>
public void DamageChanged(DamageableComponent component, DamageSpecifier? damageDelta = null)
{
component.DamagePerGroup = component.Damage.GetDamagePerGroup();
component.TotalDamage = component.Damage.Total;
component.Dirty();
RaiseLocalEvent(component.Owner.Uid, new DamageChangedEvent(component, damageDelta), false);
}
/// <summary>
/// Applies damage specified via a <see cref="DamageSpecifier"/>.
/// </summary>
/// <remarks>
/// <see cref="DamageSpecifier"/> is effectively just a dictionary of damage types and damage values. This
/// function just applies the container's resistances (unless otherwise specified) and then changes the
/// stored damage data. Division of group damage into types is managed by <see cref="DamageSpecifier"/>.
/// </remarks>
/// <returns>
/// Returns a <see cref="DamageSpecifier"/> with information about the actual damage changes. This will be
/// null if the user had no applicable components that can take damage.
/// </returns>
public DamageSpecifier? TryChangeDamage(EntityUid uid, DamageSpecifier damage, bool ignoreResistances = false)
{
if (!ComponentManager.TryGetComponent<DamageableComponent>(uid, out var damageable))
{
// TODO BODY SYSTEM pass damage onto body system
return null;
}
if (damage == null)
{
Logger.Error("Null DamageSpecifier. Probably because a required yaml field was not given.");
return null;
}
if (damage.Empty)
{
return damage;
}
// Apply resistances
if (!ignoreResistances && damageable.ResistanceSetID != null)
{
if (_prototypeManager.TryIndex<ResistanceSetPrototype>(damageable.ResistanceSetID, out var resistanceSet))
{
damage = DamageSpecifier.ApplyResistanceSet(damage, resistanceSet);
}
if (damage.Empty)
{
return damage;
}
}
// Copy the current damage, for calculating the difference
DamageSpecifier oldDamage = new(damageable.Damage);
damageable.Damage.ExclusiveAdd(damage);
damageable.Damage.ClampMin(0);
var delta = damageable.Damage - oldDamage;
delta.TrimZeros();
if (!delta.Empty)
{
DamageChanged(damageable, delta);
}
return delta;
}
/// <summary>
/// Sets all damage types supported by a <see cref="DamageableComponent"/> to the specified value.
/// </summary>
/// <remakrs>
/// Does nothing If the given damage value is negative.
/// </remakrs>
public void SetAllDamage(DamageableComponent component, int newValue)
{
if (newValue < 0)
{
// invalid value
return;
}
foreach (var type in component.Damage.DamageDict.Keys)
{
component.Damage.DamageDict[type] = newValue;
}
// Setting damage does not count as 'dealing' damage, even if it is set to a larger value, so we pass an
// empty damage delta.
DamageChanged(component, new DamageSpecifier());
}
private void DamageableGetState(EntityUid uid, DamageableComponent component, ref ComponentGetState args)
{
args.State = new DamageableComponentState(component.Damage.DamageDict, component.ResistanceSetID);
}
private void DamageableHandleState(EntityUid uid, DamageableComponent component, ref ComponentHandleState args)
{
if (args.Current is not DamageableComponentState state)
{
return;
}
component.ResistanceSetID = state.ResistanceSetID;
// Has the damage actually changed?
DamageSpecifier newDamage = new() { DamageDict = state.DamageDict };
var delta = component.Damage - newDamage;
delta.TrimZeros();
if (!delta.Empty)
{
component.Damage = newDamage;
DamageChanged(component, delta);
}
}
}
public class DamageChangedEvent : EntityEventArgs
{
/// <summary>
/// This is the component whose damage was changed.
/// </summary>
/// <remarks>
/// Given that nearly every component that cares about a change in the damage, needs to know the
/// current damage values, directly passing this information prevents a lot of duplicate
/// Owner.TryGetComponent() calls.
/// </remarks>
public readonly DamageableComponent Damageable;
/// <summary>
/// The amount by which the damage has changed. If the damage was set directly to some number, this will be
/// null.
/// </summary>
public readonly DamageSpecifier? DamageDelta;
/// <summary>
/// Was any of the damage change dealing damage, or was it all healing?
/// </summary>
public readonly bool DamageIncreased = false;
public DamageChangedEvent(DamageableComponent damageable, DamageSpecifier? damageDelta)
{
Damageable = damageable;
DamageDelta = damageDelta;
if (DamageDelta == null)
return;
foreach (var damageChange in DamageDelta.DamageDict.Values)
{
if (damageChange > 0)
{
DamageIncreased = true;
break;
}
}
}
}
}

View File

@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
using Robust.Shared.ViewVariables;
namespace Content.Shared.Damage.Prototypes
{
/// <summary>
/// A damage container which can be used to specify support for various damage types.
/// </summary>
/// <remarks>
/// This is effectively just a list of damage types that can be specified in YAML files using both damage types
/// and damage groups. Currently this is only used to specify what damage types a <see
/// cref="DamageableComponent"/> should support.
/// </remarks>
[Prototype("damageContainer")]
[Serializable, NetSerializable]
public class DamageContainerPrototype : IPrototype
{
[ViewVariables]
[DataField("id", required: true)]
public string ID { get; } = default!;
/// <summary>
/// List of damage groups that are supported by this container.
/// </summary>
[DataField("supportedGroups", customTypeSerializer: typeof(PrototypeIdListSerializer<DamageGroupPrototype>))]
public List<string> SupportedGroups = new();
/// <summary>
/// Partial List of damage types supported by this container. Note that members of the damage groups listed
/// in <see cref="SupportedGroups"/> are also supported, but they are not included in this list.
/// </summary>
[DataField("supportedTypes", customTypeSerializer: typeof(PrototypeIdListSerializer<DamageTypePrototype>))]
public List<string> SupportedTypes = new();
}
}

View File

@@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Shared.Damage.Prototypes
{
/// <summary>
/// A Group of <see cref="DamageTypePrototype"/>s.
/// </summary>
/// <remarks>
/// These groups can be used to specify supported damage types of a <see cref="DamageContainerPrototype"/>, or
/// to change/get/set damage in a <see cref="DamageableComponent"/>.
/// </remarks>
[Prototype("damageGroup")]
[Serializable, NetSerializable]
public class DamageGroupPrototype : IPrototype
{
[DataField("id", required: true)] public string ID { get; } = default!;
[DataField("damageTypes", required: true, customTypeSerializer: typeof(PrototypeIdListSerializer<DamageTypePrototype>))]
public List<string> DamageTypes { get; } = default!;
}
}

View File

@@ -3,7 +3,7 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Shared.Damage
namespace Content.Shared.Damage.Prototypes
{
/// <summary>
/// A single damage type. These types are grouped together in <see cref="DamageGroupPrototype"/>s.

View File

@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
using Robust.Shared.ViewVariables;
namespace Content.Shared.Damage.Prototypes
{
/// <summary>
/// Prototype of damage resistance sets. Can be applied to <see cref="DamageSpecifier"/> using <see
/// cref="DamageSpecifier.ApplyResistanceSet(ResistanceSetPrototype)"/>. This can be done several times as the
/// <see cref="DamageSpecifier"/> is passed to it's final target. By default the receiving <see cref="DamageableComponent"/>, will
/// also apply it's own <see cref="ResistanceSetPrototype"/>.
/// </summary>
[Prototype("resistanceSet")]
[Serializable, NetSerializable]
public class ResistanceSetPrototype : IPrototype
{
[ViewVariables]
[DataField("id", required: true)]
public string ID { get; } = default!;
[DataField("coefficients", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<float, DamageTypePrototype>))]
public Dictionary<string, float> Coefficients = new();
[DataField("flatReductions", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<float, DamageTypePrototype>))]
public Dictionary<string, float> FlatReduction = new();
}
}

View File

@@ -1,66 +0,0 @@
using System;
using System.Collections.Generic;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
namespace Content.Shared.Damage.Resistances
{
/// <summary>
/// Set of resistances used by damageable objects.
/// Each <see cref="DamageTypePrototype"/> has a multiplier and flat damage
/// reduction value.
/// </summary>
[Serializable, NetSerializable]
public class ResistanceSet
{
[ViewVariables]
public string? ID { get; } = string.Empty;
[ViewVariables]
public Dictionary<DamageTypePrototype, ResistanceSetSettings> Resistances { get; } = new();
public ResistanceSet()
{
}
public ResistanceSet(ResistanceSetPrototype data)
{
ID = data.ID;
Resistances = data.Resistances;
}
/// <summary>
/// Adjusts input damage with the resistance set values.
/// Only applies reduction if the amount is damage (positive), not
/// healing (negative).
/// </summary>
/// <param name="damageType">Type of damage.</param>
/// <param name="amount">Incoming amount of damage.</param>
public int CalculateDamage(DamageTypePrototype damageType, int amount)
{
// Do nothing if the damage type is not specified in resistance set.
if (!Resistances.TryGetValue(damageType, out var resistance))
{
return amount;
}
if (amount > 0) // Only apply reduction if it's healing, not damage.
{
amount -= resistance.FlatReduction;
if (amount <= 0)
{
return 0;
}
}
amount = (int) Math.Ceiling(amount * resistance.Coefficient);
return amount;
}
}
}

View File

@@ -1,61 +0,0 @@
using System;
using System.CodeDom;
using System.Collections.Generic;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Shared.Damage.Resistances
{
/// <summary>
/// Prototype for the BodyPart class.
/// </summary>
[Prototype("resistanceSet")]
[Serializable, NetSerializable]
public class ResistanceSetPrototype : IPrototype, ISerializationHooks
{
[ViewVariables]
[DataField("id", required: true)]
public string ID { get; } = default!;
[ViewVariables]
[DataField("coefficients", required: true)]
private Dictionary<string, float> coefficients { get; } = new();
[ViewVariables]
[DataField("flatReductions", required: true)]
private Dictionary<string, int> flatReductions { get; } = new();
[ViewVariables]
public Dictionary<DamageTypePrototype, ResistanceSetSettings> Resistances { get; private set; } = new();
void ISerializationHooks.AfterDeserialization()
{
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
foreach (var damageTypeID in coefficients.Keys)
{
var resolvedDamageType = prototypeManager.Index<DamageTypePrototype>(damageTypeID);
Resistances.Add(resolvedDamageType, new ResistanceSetSettings(coefficients[damageTypeID], flatReductions[damageTypeID]));
}
}
}
/// <summary>
/// Resistance Settings for a specific DamageType. Flat reduction should always be applied before the coefficient.
/// </summary>
[Serializable, NetSerializable]
public readonly struct ResistanceSetSettings
{
[ViewVariables] public readonly float Coefficient;
[ViewVariables] public readonly int FlatReduction;
public ResistanceSetSettings(float coefficient, int flatReduction)
{
Coefficient = coefficient;
FlatReduction = flatReduction;
}
}
}